
배포까지 한 번 끝낸 프로젝트를 다시 열어보면, 이제는 다른 종류의 아쉬움이 보이기 시작합니다. 기능은 되는데 코드가 길게 몰려 있다거나, 수정 지점을 찾기가 조금 불편하다거나, 비슷한 스타일이 여러 군데 반복되는 식입니다. 이때 많은 초보자가 "그럼 새 기능부터 더 넣어야 하나?"라고 생각하는데, 오히려 지금은 그 반대가 더 중요합니다.
이번 단계에서는 리팩터링을 해보겠습니다. 다만 거창하게 갈아엎는 방향은 아닙니다. 지금 잘 돌아가는 상태를 유지한 채, 읽기 좋고 손대기 쉬운 구조로 조금만 다듬는 쪽에 집중하겠습니다.
특히 바이브코딩에서는 이 단계가 중요합니다. AI가 코드를 빠르게 만들어줄수록, 어느 순간부터는 "만드는 속도"보다 "읽을 수 있는 구조인가"가 더 중요해지기 쉽기 때문입니다. 코드가 길게 늘어나면, 다음 수정 때 어디부터 건드려야 하는지 흐려지기 때문입니다.
| 지금 바꾸는 것 | 지금은 안 바꾸는 것 | 왜 이렇게 가나 |
|---|---|---|
| HTML 구역 이름과 배치 | 할 일 추가, 체크, 삭제 기능 | 구조를 읽기 좋게 만들되, 동작은 그대로 두기 위해서입니다 |
| CSS 반복 규칙 정리 | 디자인 전면 개편 | 겉모습보다 수정 동선을 먼저 정리하는 쪽이 낫기 때문입니다 |
| JavaScript 함수 분리 | 새 기능 추가 | 지금은 "더 만들기"보다 "다음 수정이 덜 힘들게 만들기"가 우선이기 때문입니다 |
이번 글의 핵심 한 줄
리팩터링은 멋있게 다시 쓰는 작업이 아니라, 지금 잘 되던 기능을 망치지 않으면서 다음 수정이 덜 힘들어지게 만드는 작업에 더 가깝습니다.
왜 지금 리팩터링을 해봐야 하는가

지금까지 만든 할 일 앱은 이미 기본 기능이 돌아갑니다. 그러면 얼핏 보기에는 더 이상 손댈 이유가 없어 보일 수도 있습니다. 그런데 실제로는 여기서 한 단계 더 가는 경험이 중요합니다. 돌아가는 코드를 유지하면서 구조를 정리해보는 경험이 있어야, 다음 프로젝트에서도 "어디를 어떻게 정리하면 되는지" 감이 생기기 때문입니다.
특히 바이브코딩에서는 이 단계가 더 중요합니다. AI가 코드를 빠르게 만들어줄수록, 어느 순간부터는 "만드는 속도"보다 "읽을 수 있는 구조인가"가 더 중요해지기 쉽습니다. 코드가 한 파일 안에서 길게 늘어나면, 다음 수정 때 어디부터 건드려야 하는지 흐려지기 때문입니다.
- 화면 구조와 역할이 더 또렷하게 보입니다.
- 비슷한 CSS가 여러 번 반복되는 문제를 줄일 수 있습니다.
- JavaScript를 수정할 때 어느 함수부터 봐야 하는지 쉬워집니다.
- Codex에게 수정 요청을 할 때도 범위를 더 정확하게 자를 수 있습니다.
핵심 포인트
새 기능이 없더라도, 구조를 정리하는 경험은 다음 기능을 더 안전하게 붙이기 위한 바닥을 만드는 일입니다.
리팩터링은 무엇을 바꾸고 무엇은 바꾸지 않는가
입문자가 리팩터링에서 가장 많이 헷갈리는 부분은, 기능 추가와 구조 정리를 한 번에 섞어버리는 점입니다. 이러면 무엇 때문에 문제가 생겼는지 구분하기 어려워집니다. 그래서 이번 글에서는 선을 먼저 분명하게 긋고 가는 편이 좋습니다.
이번 글에서 바꾸는 것은 아래와 같습니다.
- HTML 구역 이름과 배치를 더 읽기 좋게 정리합니다.
- CSS에서 반복되는 버튼 스타일을 공통 형태로 묶습니다.
- JavaScript에서 역할이 큰 함수를 작은 함수로 나눕니다.
반대로 이번 글에서 바꾸지 않는 것은 아래와 같습니다.
- 할 일 추가 기능
- 완료 체크 기능
- 개별 삭제 기능
- 전체 삭제 기능
- 브라우저 저장 유지 기능
즉, 이번 리팩터링의 목적은 "새로운 앱 만들기"가 아니라, 지금 있는 앱을 같은 기능으로 더 읽기 좋게 정리하는 것입니다. 이 기준이 흔들리면 금방 범위가 커집니다.
핵심 포인트
리팩터링을 할 때는 "동작은 그대로, 구조만 정리"라는 선을 먼저 그어두는 편이 좋습니다.
시작 전에 체크포인트부터 남긴다
리팩터링은 기능을 새로 넣는 작업이 아니기 때문에, 겉으로 보기엔 변화가 작아 보일 수 있습니다. 그런데 오히려 그래서 더 체크포인트가 중요합니다. 구조를 정리하다 보면 생각보다 여러 줄이 한꺼번에 바뀌고, 잘못하면 "왜 안 되지?"가 아니라 "어디가 바뀌었지?"부터 흐려질 수 있기 때문입니다.
그래서 시작 전에는 먼저 안정 상태를 하나 남겨두는 편이 좋습니다.
git status
git add .
git commit -m "리팩터링 전 안정 버전 저장"
여기서 중요한 건 명령어를 많이 아는 게 아니라, 지금 잘 되던 상태를 먼저 기록으로 고정하는 습관입니다. 그래야 리팩터링 중에 꼬여도 다시 돌아갈 기준이 생깁니다.
이 단계에서는 VS Code의 diff 화면도 같이 보는 편이 좋습니다. 리팩터링은 기능 변화보다 "무엇이 얼마나 정리됐는지"가 더 중요하기 때문에, 저장하기 전에 바뀐 줄을 차분하게 보는 습관이 꽤 도움이 됩니다.
핵심 포인트
리팩터링 전 커밋은 "나중에 올리기 위한 기록"이 아니라, 문제가 생겼을 때 다시 돌아갈 수 있는 바닥에 더 가깝습니다.
15분 실습: 먼저 냄새부터 찾기
리팩터링은 코드부터 막 건드린다고 잘 되는 게 아닙니다. 먼저 "어디가 불편한지"를 눈으로 찾아야 합니다. 초보자 기준에서는 아래 세 가지가 가장 찾기 쉽습니다.
- 같은 구조가 여러 번 반복된다.
- 한 함수 안에서 하는 일이 너무 많다.
- 클래스 이름이나 구역 이름이 역할을 잘 설명하지 못한다.
예를 들어 아래 JavaScript는 돌아가긴 하지만, 한 함수 안에서 너무 많은 일을 한꺼번에 처리하고 있습니다.
function renderTodos() {
todoList.innerHTML = "";
if (todos.length === 0) {
message.textContent = "아직 등록된 할 일이 없습니다.";
return;
}
todos.forEach(function (todo) {
const item = document.createElement("li");
const left = document.createElement("div");
const checkbox = document.createElement("input");
const text = document.createElement("span");
const deleteButton = document.createElement("button");
item.className = "todo-item";
left.className = "todo-left";
if (todo.done) {
item.classList.add("done");
}
checkbox.type = "checkbox";
checkbox.checked = todo.done;
checkbox.addEventListener("change", function () {
todo.done = checkbox.checked;
saveTodos();
renderTodos();
});
text.textContent = todo.text;
deleteButton.type = "button";
deleteButton.className = "delete-button";
deleteButton.textContent = "삭제";
deleteButton.addEventListener("click", function () {
todos = todos.filter(function (currentTodo) {
return currentTodo.id !== todo.id;
});
saveTodos();
renderTodos();
});
left.append(checkbox, text);
item.append(left, deleteButton);
todoList.append(item);
});
message.textContent = "체크하거나 삭제해보세요.";
}
이 코드가 틀린 건 아닙니다. 다만 초보자 기준에서는 이런 질문을 먼저 던져볼 수 있습니다.
- 체크박스 만드는 일과 삭제 버튼 만드는 일을 따로 뺄 수 없을까
- 목록 한 줄 만드는 일을 별도 함수로 나눌 수 없을까
- 안내 문구 바꾸는 일은 따로 떼어낼 수 없을까
리팩터링은 바로 이런 질문에서 시작하는 편이 좋습니다. "코드가 길다"가 아니라, 무엇이 한데 몰려 있는지부터 보는 것이 먼저입니다.
핵심 포인트
리팩터링은 "이 코드는 이상하다"보다 "이 역할을 따로 뺄 수 있을까?"라는 질문에서 시작하는 편이 훨씬 좋습니다.
HTML 구조를 조금 더 읽기 좋게 정리하기
먼저 HTML부터 정리해보겠습니다. 이번에는 화면이 완전히 달라지지는 않습니다. 대신 역할이 드러나도록 구역을 조금 더 분명하게 나누겠습니다. 이 변화가 중요한 이유는, 나중에 JavaScript가 어떤 영역을 업데이트하는지 읽기가 훨씬 쉬워지기 때문입니다.
리팩터링 전 HTML이 아래처럼 있었다고 해보겠습니다.
<main class="app">
<section class="panel">
<p class="eyebrow">Mini Project Update</p>
<h1>오늘 할 일</h1>
<p class="description">
배포한 앱을 크게 뜯지 않고, 작은 개선부터 붙여보는 단계입니다.
</p>
<form id="todoForm" class="todo-form">
<label for="todoInput">할 일 입력</label>
<div class="input-row">
<input id="todoInput" type="text">
<button type="submit">추가</button>
</div>
</form>
<section class="toolbar">
<p id="summaryText" class="summary">남은 할 일 0개</p>
<button id="clearAllButton" type="button" class="secondary-button">
전체 삭제
</button>
</section>
<p id="message" class="message">할 일을 입력하고 추가 버튼을 눌러보세요.</p>
<ul id="todoList" class="todo-list"></ul>
</section>
</main>
이 구조도 괜찮지만, 역할이 조금 더 드러나게 정리하면 아래처럼 바꿔볼 수 있습니다.
<main class="app">
<section class="panel">
<div class="panel-header">
<p class="eyebrow">Mini Project Update</p>
<h1>오늘 할 일</h1>
<p class="description">
배포한 앱을 크게 뜯지 않고, 작은 개선부터 붙여보는 단계입니다.
</p>
</div>
<div class="composer">
<form id="todoForm" class="todo-form">
<label for="todoInput">할 일 입력</label>
<div class="input-row">
<input id="todoInput" type="text">
<button type="submit">추가</button>
</div>
</form>
</div>
<div class="toolbar">
<p id="summaryText" class="summary">남은 할 일 0개</p>
<button id="clearAllButton" type="button" class="secondary-button">
전체 삭제
</button>
</div>
<p id="message" class="message">할 일을 입력하고 추가 버튼을 눌러보세요.</p>
<div class="list-section">
<ul id="todoList" class="todo-list"></ul>
</div>
</section>
</main>
이번 HTML 수정에서 눈여겨볼 부분은 세 군데입니다.
- panel-header : 제목과 설명 영역을 따로 묶었습니다.
- composer : 입력 폼 구역을 따로 분리했습니다.
- list-section : 실제 목록이 들어가는 영역을 이름으로 드러냈습니다.
이렇게 해두면 화면이 크게 달라지지 않아도 구조가 훨씬 또렷해집니다. 특히 나중에 Codex에게 수정 요청을 할 때도 "입력 구역만 건드려줘" 같은 식으로 더 정확하게 말하기 쉬워집니다.
핵심 포인트
HTML 리팩터링의 핵심은 태그를 복잡하게 늘리는 게 아니라, 구역의 역할이 이름만 봐도 보이게 만드는 것입니다.
CSS를 공통 스타일 중심으로 정리하기
이제 CSS를 보겠습니다. 지금까지의 CSS도 동작에는 문제가 없었지만, 버튼 스타일처럼 비슷한 규칙이 조금씩 흩어져 있으면 다음 수정 때 손이 여러 군데로 갑니다. 그래서 이번에는 반복되는 스타일을 조금 더 공통적으로 묶는 방향으로 정리해보겠습니다.
리팩터링 전 CSS가 아래처럼 나뉘어 있었다고 해보겠습니다.
button {
border: none;
border-radius: 14px;
padding: 14px 18px;
font: inherit;
font-weight: 700;
cursor: pointer;
background: #111827;
color: #ffffff;
}
.secondary-button {
background: #ffffff;
color: #111827;
border: 1px solid #d1d5db;
}
.delete-button {
background: #ffffff;
color: #111827;
border: 1px solid #d1d5db;
}
이 경우 공통 규칙을 조금 더 분명하게 묶으면 아래처럼 읽히게 바꿀 수 있습니다.
button {
font: inherit;
}
button:not(.secondary-button):not(.delete-button) {
border: none;
border-radius: 14px;
padding: 14px 18px;
font-weight: 700;
cursor: pointer;
background: #111827;
color: #ffffff;
}
.secondary-button,
.delete-button {
border-radius: 14px;
padding: 14px 18px;
font-weight: 700;
cursor: pointer;
background: #ffffff;
color: #111827;
border: 1px solid #d1d5db;
}
이번 CSS 리팩터링에서 중요한 건 디자인 자체보다 중복을 줄이는 방식입니다.
- 버튼 공통 규칙을 한 번에 묶었습니다.
- 구역 간 간격을 역할별로 나눠서 읽기 쉽게 했습니다.
- 입력, 툴바, 목록 영역이 각각 어디서 시작되는지 보이게 했습니다.
이렇게 정리하면 다음에 버튼 스타일을 바꾸고 싶을 때도 여러 군데를 뒤질 필요가 줄어듭니다. 초보자일수록 이 차이가 꽤 크게 느껴집니다.
핵심 포인트
CSS 리팩터링은 예쁘게 만드는 작업이 아니라, 같은 성격의 규칙을 한데 모아 다음 수정이 덜 번거롭게 만드는 작업에 더 가깝습니다.
JavaScript를 작은 함수로 나누기
이번 편의 핵심은 사실 JavaScript입니다. 지금까지의 스크립트도 동작에는 문제가 없었지만, 렌더링과 이벤트 처리와 상태 변경이 한 군데에 길게 몰려 있으면 읽는 쪽이 먼저 지칩니다. 그래서 이번에는 역할별로 작은 함수로 나누는 방식으로 정리해보겠습니다.
리팩터링 전에는 이런 식으로 한 함수 안에 몰려 있었을 수 있습니다.
function renderTodos() {
todoList.innerHTML = "";
if (todos.length === 0) {
showMessage("아직 등록된 할 일이 없습니다.");
return;
}
todos.forEach(function (todo) {
const item = document.createElement("li");
const left = document.createElement("div");
const checkbox = document.createElement("input");
const text = document.createElement("span");
const deleteButton = document.createElement("button");
item.className = "todo-item";
left.className = "todo-left";
if (todo.done) {
item.classList.add("done");
}
checkbox.type = "checkbox";
checkbox.checked = todo.done;
checkbox.addEventListener("change", function () {
todo.done = checkbox.checked;
saveTodos();
renderTodos();
});
text.textContent = todo.text;
deleteButton.type = "button";
deleteButton.className = "delete-button";
deleteButton.textContent = "삭제";
deleteButton.addEventListener("click", function () {
todos = todos.filter(function (currentTodo) {
return currentTodo.id !== todo.id;
});
saveTodos();
renderTodos();
});
left.append(checkbox, text);
item.append(left, deleteButton);
todoList.appendChild(item);
});
showMessage("체크하거나 삭제해보세요.");
}

이걸 역할별로 나누면 아래처럼 볼 수 있습니다.
function createCheckbox(todo) {
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = todo.done;
checkbox.addEventListener("change", function () {
todo.done = checkbox.checked;
saveTodos();
renderTodos("할 일 상태를 업데이트했습니다.");
});
return checkbox;
}
function createDeleteButton(todoId) {
const button = document.createElement("button");
button.type = "button";
button.className = "delete-button";
button.textContent = "삭제";
button.addEventListener("click", function () {
todos = todos.filter(function (currentTodo) {
return currentTodo.id !== todoId;
});
saveTodos();
renderTodos("항목을 삭제했습니다.");
});
return button;
}
function createTodoItem(todo) {
const item = document.createElement("li");
const left = document.createElement("div");
const text = document.createElement("span");
item.className = "todo-item";
left.className = "todo-left";
if (todo.done) {
item.classList.add("done");
}
text.textContent = todo.text;
left.append(
createCheckbox(todo),
text
);
item.append(
left,
createDeleteButton(todo.id)
);
return item;
}
function renderTodos(messageText) {
todoList.innerHTML = "";
if (todos.length === 0) {
showMessage(messageText || "아직 등록된 할 일이 없습니다.");
return;
}
todos.forEach(function (todo) {
todoList.appendChild(
createTodoItem(todo)
);
});
showMessage(
messageText ||
"체크해서 완료 처리하거나, 필요 없으면 삭제해보세요."
);
}
이 코드를 읽을 때는 처음부터 끝까지 문법으로 보지 않는 편이 좋습니다. 대신 아래처럼 역할 단위로 보면 훨씬 편합니다.
- createCheckbox : 완료 상태를 바꾸는 일만 맡습니다.
- createDeleteButton : 삭제 동작만 맡습니다.
- createTodoItem : 목록 한 줄을 조립하는 일만 맡습니다.
- renderTodos : 현재 배열을 화면에 다시 그리는 일만 맡습니다.
이번 리팩터링의 핵심 흐름은 아래처럼 요약할 수 있습니다.
저장 관련 함수
화면 표시 함수
목록 항목 생성 함수
사용자 동작 처리 함수
이렇게 역할을 나누면
다음 수정에서 어디부터 봐야 할지가 훨씬 쉬워집니다.
핵심 포인트
JavaScript 리팩터링에서 중요한 건 문법을 어렵게 쓰는 게 아니라, 기능을 역할별로 나눠서 다음 수정이 덜 불편한 상태를 만드는 것입니다.
VS Code에서는 diff부터 본다
리팩터링 단계에서는 VS Code 사용 방식도 조금 달라지는 편이 좋습니다. 기능 추가 때는 "무엇이 생겼는지"가 더 중요했다면, 리팩터링에서는 무엇이 어떤 줄에서 어떻게 정리됐는지를 보는 게 더 중요합니다. 그래서 이때는 전체 파일보다 diff부터 여는 습관이 훨씬 좋습니다.
기본 순서는 단순합니다.
- Source Control을 엽니다.
- 변경된 파일을 클릭합니다.
- 왼쪽과 오른쪽을 비교하면서 어떤 줄이 사라지고, 어떤 줄이 새로 생겼는지 봅니다.
- "기능이 바뀐 줄"이 아니라 "구조가 정리된 줄"에 먼저 시선을 둡니다.
특히 리팩터링에서는 아래 질문이 꽤 잘 맞습니다.
- 같은 역할이 한데 모였는가
- 이름이 더 분명해졌는가
- 한 함수가 너무 많은 일을 안 하게 되었는가
즉, 이 단계에서는 "몇 줄이 바뀌었나"보다 바뀐 줄이 더 읽기 쉬운 구조를 만들었나를 보는 편이 훨씬 낫습니다.

핵심 포인트
리팩터링을 할 때는 전체 파일보다 diff로 바뀐 줄만 보면서 구조가 더 또렷해졌는지를 확인하는 편이 훨씬 좋습니다.
Codex에게는 이렇게 잘라서 요청하면 된다
리팩터링 단계에서도 Codex를 쓸 수 있습니다. 다만 이때는 기능 추가 요청보다 훨씬 더 조심스럽게 범위를 잘라야 합니다. "예쁘게 정리해줘"처럼 넓게 말하면, 구조 정리보다 기능까지 건드릴 가능성이 커집니다. 그래서 이번 단계에서는 어디를, 무엇만, 어디까지 바꿀지를 더 분명하게 말하는 편이 좋습니다.
예를 들어 전체 방향을 먼저 설명받고 싶다면 이렇게 물어볼 수 있습니다.
지금 할 일 앱은 기능이 정상 동작하는 상태야.
추가, 완료 체크, 삭제, 전체 삭제, 저장 기능은 그대로 유지해야 해.
이번에는 새 기능을 넣지 말고
HTML 구조 이름 정리, CSS 공통 스타일 정리,
JavaScript 함수 분리만 해줘.
먼저 어떤 파일을 왜 바꿀지부터 설명해줘.
JavaScript만 더 좁게 요청하고 싶다면 이런 식이 좋습니다.
@script.js
지금 기능은 그대로 두고
renderTodos에 몰린 역할을
작은 함수들로 나눠줘.
저장 방식은 바꾸지 말고
추가, 삭제, 전체 삭제 동작도 유지해줘.
수정 전후에 어떤 함수가 새로 생기는지 먼저 설명해줘.
CSS만 다듬고 싶다면 이렇게 더 잘라서 말할 수 있습니다.
@style.css
디자인을 완전히 바꾸지 말고
버튼 스타일 중복과 섹션 간격만 정리해줘.
모바일 동작은 그대로 유지해야 하고
클래스 이름이 바뀌면 HTML도 같이 맞춰줘.
먼저 바뀌는 선택자부터 짧게 정리해줘.
이런 식으로 요청하면 AI도 구조 정리와 기능 변경을 섞어버릴 가능성이 줄어듭니다. 초보자 입장에서는 이 차이가 꽤 큽니다.
핵심 포인트
리팩터링을 Codex에게 맡길 때는 "정리해줘"보다 무엇은 유지하고, 무엇만 정리할지를 같이 주는 편이 훨씬 안전합니다.
마무리
이번 편에서 한 일은 겉으로 보면 화려하지 않습니다. 앱에 새로운 기능이 생긴 것도 아니고, 화면이 완전히 달라진 것도 아닙니다. 그런데 바로 이런 작업이 프로젝트를 오래 가져갈 수 있게 만듭니다. 기능은 그대로 두고 구조만 정리해보는 경험이 있어야, 다음 수정이 덜 두렵고 덜 지저분해집니다.
초보자에게 리팩터링은 "잘하는 사람만 하는 작업"이 아닙니다. 오히려 지금처럼 작은 프로젝트일 때부터 조금씩 해보는 편이 훨씬 좋습니다. 구조가 작으니 무엇이 달라졌는지 따라가기 쉽고, 실수했을 때도 돌아가기 쉽기 때문입니다.
여기까지 직접 해보면, 새 기능을 넣지 않아도 프로젝트가 충분히 더 좋아질 수 있다는 감각이 생깁니다. 이 감각이 붙으면 다음부터는 무조건 뭔가를 더 붙이기보다, 지금 있는 구조를 한 번 더 읽고 정리해보는 눈도 같이 생기기 시작합니다. 그 차이가 생각보다 큽니다.
'바이브코딩 > 실습' 카테고리의 다른 글
| 바이브코딩 13편: 할 일 앱에 수정 기능 붙이기 (0) | 2026.03.30 |
|---|---|
| 바이브코딩 12편: 할 일 앱에 전체·진행 중·완료 필터 붙이기 (0) | 2026.03.27 |
| 바이브코딩 10편: 배포한 할 일 앱을 작게 개선해보기 (0) | 2026.03.23 |
| 바이브코딩 9편: 만든 할 일 앱을 인터넷에 직접 올려보기 (0) | 2026.03.20 |
| 바이브코딩 8편: 할 일 체크리스트 앱 하나를 끝까지 만들어보자 (0) | 2026.03.18 |
