
리팩터링까지 한 번 거친 프로젝트를 다시 보면, 이제는 새 기능을 붙일 준비가 조금씩 된 상태라고 볼 수 있습니다. 다만 여기서 중요한 건 여전히 같습니다. 기능을 크게 키우는 것보다, 작고 분명한 기능 하나를 정확하게 넣는 것이 더 중요합니다.
초보자가 이 단계에서 가장 자주 흔들리는 이유는 "이제는 뭔가 더 복잡한 걸 해야 할 것 같다"는 생각 때문입니다. 그런데 실제로는 반대일 때가 많습니다. 지금은 기능을 하나 더 넣더라도, 기존 구조를 유지하면서 어디에 무엇이 추가되는지 따라갈 수 있는 크기여야 합니다. 그래야 다음 수정에서도 덜 흔들립니다.
이번 글에서는 지난 편에서 정리한 할 일 앱에 필터를 붙여보겠습니다. 이 기능은 작아 보이지만, 실제로는 꽤 좋은 연습입니다. 현재 값을 따로 기억해두고, 같은 목록이라도 어떤 기준으로 보여줄지 바꿔야 하기 때문입니다. 즉, 단순히 버튼 하나 더 넣는 게 아니라 상태와 화면 표시를 연결하는 감각을 익히는 데 잘 맞는 기능입니다.
| 겉으로 보이는 변화 | 실제로 추가되는 것 | 가장 먼저 봐야 할 지점 |
|---|---|---|
| 전체, 진행 중, 완료 버튼이 생긴다 | currentFilter 같은 상태가 하나 더 생긴다 | 지금 어떤 기준으로 목록을 보여주는지 따로 기억하는지 본다 |
| 같은 목록이라도 보이는 항목이 달라진다 | 현재 필터 기준으로 실제 보여줄 배열만 다시 골라낸다 | 전체 목록을 바꾸는 게 아니라, 보여주는 기준만 달라지는지 본다 |
| 활성 버튼이 눈에 띄게 바뀐다 | 버튼 상태와 화면 상태가 같이 움직인다 | 선택된 필터가 UI에서도 분명하게 보이는지 확인한 다 |

이번 글의 핵심 한 줄
이번 편의 핵심은 버튼을 세 개 더 만드는 일이 아니라, 현재 필터 상태를 따로 들고 있으면서 화면에 어떤 목록을 보여줄지 다시 고르는 구조를 만드는 데 있습니다.
왜 지금은 큰 기능보다 작은 기능 하나가 더 중요한가
배포도 해봤고, 구조 정리도 조금 해봤다면 이제 기능 하나쯤 더 붙여보고 싶어지는 게 자연스럽습니다. 그런데 이 시점에서 갑자기 카테고리, 검색, 우선순위, 날짜 관리까지 한꺼번에 넣으려고 하면 금방 흐름이 꼬이기 쉽습니다. 초보자일수록 기존 기능이 계속 잘 되는 상태를 유지하면서 작은 기능 하나를 붙이는 경험이 더 중요합니다.
작은 기능 하나를 붙여보는 과정에는 생각보다 많은 것이 들어 있습니다. HTML에 버튼을 추가하고, CSS로 현재 선택 상태를 보이게 만들고, JavaScript에서 현재 필터 상태를 따로 들고 있어야 합니다. 즉, 겉보기에는 작은 기능이지만 구조, 스타일, 동작이 모두 연결됩니다.
- 기존 기능을 유지하면서 새 기능을 붙이는 감각을 익힐 수 있습니다.
- 어디까지가 UI이고 어디부터가 상태 관리인지 조금 더 분명해집니다.
- Codex에게도 더 구체적이고 안전한 요청을 할 수 있게 됩니다.
- 배포한 프로젝트를 "살아 있는 결과물"처럼 조금씩 키워가는 경험이 생깁니다.
핵심 포인트
지금 단계에서 중요한 건 기능 수가 아니라, 하나의 기능을 기존 구조 안에 자연스럽게 붙이는 경험입니다.
이번 편에서는 어디까지 만들 것인가
이번 편에서는 보기 필터를 추가합니다. 할 일 목록을 아래 세 가지 기준으로 볼 수 있게 만드는 기능입니다.
- 전체 : 모든 항목을 보여줍니다.
- 진행 중 : 아직 완료되지 않은 항목만 보여줍니다.
- 완료 : 체크가 끝난 항목만 보여줍니다.
이 기능이 좋은 이유는 결과가 바로 눈에 보이기 때문입니다. 버튼 하나를 누르면 같은 목록이라도 다르게 보이고, 완료 체크를 바꾸면 현재 필터에 따라 목록이 바로 달라집니다. 초보자 입장에서는 이런 즉각적인 반응이 구조를 이해하는 데 꽤 도움이 됩니다.
이번 편에서도 바꾸지 않는 것은 분명히 두고 가겠습니다.
- 할 일 추가 기능은 그대로 유지합니다.
- 완료 체크와 삭제 기능도 그대로 유지합니다.
- 전체 삭제와 브라우저 저장 기능도 그대로 둡니다.
즉, 이번 편은 "새 앱 만들기"가 아니라, 현재 앱 위에 보기 방식 하나를 더 얹는 작업입니다.
10분 실습: 필터가 왜 상태 기능인지 먼저 느껴보기

코드를 보기 전에 아주 짧게 상상 실험부터 해보는 편이 좋습니다. 아래처럼 세 개의 할 일을 만든다고 가정해보겠습니다.
1. 장보기 - 미완료
2. 메일 답장 - 완료
3. 운동하기 - 미완료
이 상태에서 "전체"를 누르면 세 개가 다 보여야 합니다. "진행 중"을 누르면 1번과 3번만 보여야 하고, "완료"를 누르면 2번만 보여야 합니다.
여기서 중요한 건 목록 자체를 지우거나 새로 만드는 게 아니라, 지금 어떤 기준으로 보여줄지를 따로 정한다는 점입니다. 즉, 데이터는 그대로 있고, 화면에 보이는 결과만 달라집니다. 이 감각이 바로 필터 기능의 핵심입니다.
실제로 구현에서도 똑같습니다.
- todos 배열은 그대로 둡니다.
- currentFilter 같은 현재 기준을 하나 따로 둡니다.
- 그 기준에 맞는 항목만 골라서 렌더링합니다.
즉, 필터는 목록을 바꾸는 기능이라기보다, 보여주는 기준을 바꾸는 기능에 더 가깝습니다. 이 차이를 먼저 잡고 들어가면 코드가 훨씬 덜 복잡하게 느껴집니다.
핵심 포인트
필터를 구현할 때는 "항목을 없앤다"보다 "같은 항목을 다른 기준으로 보여준다"고 이해하는 편이 훨씬 좋습니다.
HTML 수정: 필터 버튼 영역 추가
먼저 HTML에서는 필터 버튼을 둘 영역만 추가하면 됩니다. 입력창과 툴바 아래에 따로 두면, 현재 상태와 보기 전환이 조금 더 분명하게 구분됩니다.
index.html은 아래처럼 정리해보겠습니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>오늘 할 일 앱</title>
<link rel="stylesheet" href="style.css">
<script defer src="script.js"></script>
</head>
<body>
<main class="app">
<section class="panel">
<div class="panel-header">
<p class="eyebrow">Filter Practice</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"
placeholder="예: 필터 기능 점검하기"
>
<button
type="submit"
class="primary-button"
>
추가
</button>
</div>
</form>
</div>
<div class="toolbar">
<p
id="summaryText"
class="summary"
>
전체 0개 · 남은 할 일 0개
</p>
<button
id="clearAllButton"
type="button"
class="secondary-button"
>
전체 삭제
</button>
</div>
<div class="filter-section">
<div
class="filter-group"
role="group"
aria-label="필터 선택"
>
<button
type="button"
class="filter-button is-active"
data-filter="all"
aria-pressed="true"
>
전체
</button>
<button
type="button"
class="filter-button"
data-filter="active"
aria-pressed="false"
>
진행 중
</button>
<button
type="button"
class="filter-button"
data-filter="completed"
aria-pressed="false"
>
완료
</button>
</div>
</div>
<p
id="message"
class="message"
>
할 일을 입력하고 추가 버튼을 눌러보세요.
</p>
<div class="list-section">
<ul
id="todoList"
class="todo-list"
></ul>
</div>
</section>
</main>
</body>
</html>
이번 HTML 수정에서 핵심은 필터 버튼 세 개가 아니라, 지금 어떤 보기 상태인지 구분할 수 있는 구역이 따로 생겼다는 점입니다. 즉, 사용자가 앱을 "입력하는 구역", "상태를 보는 구역", "보기 방식을 바꾸는 구역"으로 나눠서 볼 수 있게 됩니다.
또 하나 눈에 익혀둘 부분은 data-filter입니다. 이 속성 덕분에 JavaScript에서 "이 버튼이 전체용인지, 진행 중용인지, 완료용인지"를 구분하기 쉬워집니다. 초보자 기준에서는 이런 연결이 눈에 보이는 방식이 이해하기 편합니다.
핵심 포인트
HTML 단계에서는 필터 버튼을 단순히 늘리는 것보다, JavaScript가 어떤 버튼이 어떤 역할인지 읽기 쉬운 구조로 두는 것이 더 중요합니다.
CSS 수정: 활성 필터 상태가 보이게 만들기

이번 기능은 버튼을 누르는 것만큼이나, 지금 어떤 필터가 선택돼 있는지 눈에 보여야 제대로 쓸 수 있습니다. 그래서 CSS에서는 필터 버튼의 기본 상태와 활성 상태를 구분해주는 쪽이 핵심입니다.
style.css는 아래처럼 정리해보겠습니다.
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;
background: #f3f4f6;
color: #111827;
}
.app {
width: min(760px, calc(100% - 32px));
margin: 48px auto;
}
.panel {
background: #ffffff;
border-radius: 20px;
padding: 32px;
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08);
}
.panel-header {
margin-bottom: 24px;
}
.eyebrow {
margin: 0 0 12px;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #6b7280;
}
h1 {
margin: 0 0 12px;
font-size: 32px;
line-height: 1.2;
}
.description {
margin: 0;
line-height: 1.7;
color: #374151;
}
.composer {
margin-top: 24px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 700;
}
.input-row {
display: flex;
gap: 12px;
}
input[type="text"] {
flex: 1;
padding: 14px 16px;
border: 1px solid #d1d5db;
border-radius: 14px;
font: inherit;
}
button {
font: inherit;
}
.primary-button {
border: none;
border-radius: 14px;
padding: 14px 18px;
font-weight: 700;
cursor: pointer;
background: #111827;
color: #ffffff;
}
.secondary-button,
.delete-button,
.filter-button {
border-radius: 14px;
padding: 12px 14px;
font-weight: 700;
cursor: pointer;
background: #ffffff;
color: #111827;
border: 1px solid #d1d5db;
}
.secondary-button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.toolbar {
margin-top: 18px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.summary {
margin: 0;
color: #111827;
font-weight: 700;
line-height: 1.6;
}
.filter-section {
margin-top: 16px;
}
.filter-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-button {
border-radius: 999px;
}
.filter-button.is-active {
background: #111827;
color: #ffffff;
border-color: #111827;
}
.message {
margin: 14px 0 0;
line-height: 1.6;
color: #6b7280;
}
.list-section {
margin-top: 24px;
}
.todo-list {
list-style: none;
margin: 0;
padding: 0;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 16px 18px;
border: 1px solid #e5e7eb;
border-radius: 16px;
background: #f9fafb;
}
.todo-item + .todo-item {
margin-top: 12px;
}
.todo-left {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.todo-left span {
line-height: 1.6;
word-break: break-word;
}
.todo-item.done span {
text-decoration: line-through;
color: #9ca3af;
}
.delete-button {
background: #ffffff;
color: #111827;
border: 1px solid #d1d5db;
}
@media (max-width: 640px) {
.app {
margin: 32px auto;
}
.panel {
padding: 24px;
}
.input-row {
flex-direction: column;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.filter-group {
width: 100%;
}
.filter-button {
flex: 1 1 auto;
}
.todo-item {
flex-direction: column;
align-items: stretch;
}
}
이번 CSS에서 가장 중요한 부분은 filter-button과 filter-button is-active입니다. 두 상태가 구분되지 않으면 필터는 눌러도 체감이 약합니다. 반대로 선택된 버튼이 또렷하게 보이면, 지금 어떤 기준으로 목록을 보고 있는지 훨씬 쉽게 읽힙니다.
이런 수정은 화려한 디자인 작업과는 조금 다릅니다. 이번 목적은 예쁘게 꾸미는 것보다 상태가 더 분명하게 읽히는 화면을 만드는 쪽에 가깝습니다.
핵심 포인트
필터 UI는 버튼을 많이 만드는 게 아니라, 지금 어떤 기준이 선택됐는지 한눈에 보이게 만드는 것이 핵심입니다.
JavaScript 수정: 필터 상태와 목록 렌더링 연결하기
이번 편의 핵심은 JavaScript입니다. 왜냐하면 필터 기능은 결국 지금 어떤 보기 상태를 선택했는지를 따로 기억하고, 그 상태에 맞게 목록을 다시 그려주는 작업이기 때문입니다.
| 이름 | 역할 | 왜 중요한가 |
|---|---|---|
| FILTER_TYPES | 필터 종류를 한곳에 모아둡니다 | 문자열을 여기저기 흩뿌리지 않게 해줍니다 |
| currentFilter | 현재 어떤 필터가 선택돼 있는지 기억합니다 | 필터 기능의 핵심 상태입니다 |
| getFilteredTodos | 현재 기준에 맞는 항목만 골라냅니다 | 화면에 실제로 보일 목록을 정합니다 |
| updateFilterButtons | 선택된 버튼 상태를 UI에 반영합니다 | 필터 상태와 버튼 상태가 따로 놀지 않게 해줍니다 |
script.js는 아래처럼 정리해보겠습니다.
const STORAGE_KEY = "vibe-todos";
const FILTER_TYPES = {
ALL: "all",
ACTIVE: "active",
COMPLETED: "completed"
};
const elements = {
form: document.querySelector("#todoForm"),
input: document.querySelector("#todoInput"),
list: document.querySelector("#todoList"),
message: document.querySelector("#message"),
summary: document.querySelector("#summaryText"),
clearAllButton: document.querySelector("#clearAllButton"),
filterButtons: Array.from(
document.querySelectorAll("[data-filter]")
)
};
let todos = loadTodos();
let currentFilter = FILTER_TYPES.ALL;
function loadTodos() {
try {
const savedTodos = localStorage.getItem(STORAGE_KEY);
return savedTodos ? JSON.parse(savedTodos) : [];
} catch (error) {
return [];
}
}
function saveTodos() {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(todos)
);
}
function setMessage(text) {
elements.message.textContent = text;
}
function getRemainingCount() {
return todos.filter(function (todo) {
return !todo.done;
}).length;
}
function getFilteredTodos() {
if (currentFilter === FILTER_TYPES.ACTIVE) {
return todos.filter(function (todo) {
return !todo.done;
});
}
if (currentFilter === FILTER_TYPES.COMPLETED) {
return todos.filter(function (todo) {
return todo.done;
});
}
return todos;
}
function updateSummary() {
const totalCount = todos.length;
const remainingCount = getRemainingCount();
elements.summary.textContent =
"전체 " +
totalCount +
"개 · 남은 할 일 " +
remainingCount +
"개";
elements.clearAllButton.disabled = totalCount === 0;
}
function updateFilterButtons() {
elements.filterButtons.forEach(function (button) {
const isActive =
button.dataset.filter === currentFilter;
button.classList.toggle("is-active", isActive);
button.setAttribute(
"aria-pressed",
String(isActive)
);
});
}
function getDefaultMessage() {
if (todos.length === 0) {
return "아직 등록된 할 일이 없습니다.";
}
if (currentFilter === FILTER_TYPES.ACTIVE) {
return "진행 중인 할 일만 보고 있습니다.";
}
if (currentFilter === FILTER_TYPES.COMPLETED) {
return "완료한 할 일만 보고 있습니다.";
}
return "체크해서 완료 처리하거나, 필요 없으면 삭제해보세요.";
}
function getEmptyFilterMessage() {
if (todos.length === 0) {
return "아직 등록된 할 일이 없습니다.";
}
if (currentFilter === FILTER_TYPES.ACTIVE) {
return "진행 중인 할 일이 없습니다.";
}
if (currentFilter === FILTER_TYPES.COMPLETED) {
return "완료한 할 일이 없습니다.";
}
return "아직 등록된 할 일이 없습니다.";
}
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 createTodoText(todo) {
const text = document.createElement("span");
text.textContent = todo.text;
return text;
}
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");
item.className = "todo-item";
left.className = "todo-left";
if (todo.done) {
item.classList.add("done");
}
left.append(
createCheckbox(todo),
createTodoText(todo)
);
item.append(
left,
createDeleteButton(todo.id)
);
return item;
}
function renderTodos(messageText) {
const visibleTodos = getFilteredTodos();
elements.list.innerHTML = "";
updateSummary();
updateFilterButtons();
if (visibleTodos.length === 0) {
setMessage(messageText || getEmptyFilterMessage());
return;
}
visibleTodos.forEach(function (todo) {
elements.list.appendChild(
createTodoItem(todo)
);
});
setMessage(messageText || getDefaultMessage());
}
function addTodo(todoText) {
todos.unshift({
id: Date.now(),
text: todoText,
done: false
});
currentFilter = FILTER_TYPES.ALL;
saveTodos();
renderTodos("할 일을 추가했습니다.");
}
function clearAllTodos() {
if (todos.length === 0) {
return;
}
const shouldClear = window.confirm(
"정말 모든 할 일을 삭제하시겠습니까?"
);
if (!shouldClear) {
setMessage("전체 삭제를 취소했습니다.");
return;
}
todos = [];
currentFilter = FILTER_TYPES.ALL;
saveTodos();
renderTodos("모든 항목을 삭제했습니다.");
}
function setFilter(nextFilter) {
if (currentFilter === nextFilter) {
return;
}
currentFilter = nextFilter;
renderTodos();
}
function handleSubmit(event) {
event.preventDefault();
const text = elements.input.value.trim();
if (!text) {
setMessage("할 일을 먼저 입력해보세요.");
elements.input.focus();
return;
}
addTodo(text);
elements.input.value = "";
elements.input.focus();
}
function bindFilterEvents() {
elements.filterButtons.forEach(function (button) {
button.addEventListener("click", function () {
setFilter(button.dataset.filter);
});
});
}
function bindEvents() {
elements.form.addEventListener(
"submit",
handleSubmit
);
elements.clearAllButton.addEventListener(
"click",
clearAllTodos
);
bindFilterEvents();
}
bindEvents();
renderTodos();
이번 코드에서 가장 먼저 봐야 할 부분은 세 군데입니다.
- currentFilter : 지금 어떤 보기 상태인지 기억하는 값입니다.
- getFilteredTodos : 현재 필터 기준으로 실제 보여줄 목록만 골라냅니다.
- updateFilterButtons : 버튼의 활성 상태를 화면에 반영합니다.
즉, 이번 기능의 핵심 흐름은 이렇습니다. 버튼을 누르면 현재 필터 상태가 바뀌고, 그 상태에 맞게 보여줄 목록을 다시 고르고, 화면도 같이 갱신됩니다. 보기 버튼이 눈에 띄게 바뀌는 것도 같은 흐름 안에 들어 있습니다.
그리고 한 가지 더 중요한 점이 있습니다. 새 할 일을 추가할 때는 일부러 전체 보기로 돌아오게 만들었습니다. 예를 들어 "완료" 필터를 켜둔 상태에서 새 할 일을 추가하면, 방금 만든 항목이 바로 안 보여서 초보자가 당황할 수 있기 때문입니다. 이런 작은 판단도 실제 사용감에는 꽤 크게 작용합니다.
핵심 정리
이번 편의 JavaScript는 버튼을 세 개 더 만드는 작업이 아니라, 현재 필터 상태를 따로 들고 있으면서 화면에 보일 배열을 다시 고르는 작업에 더 가깝습니다.
직접 눌러보며 테스트하기

이번 기능은 겉으로 보기엔 간단하지만, 실제로는 현재 상태와 화면 갱신이 같이 움직입니다. 그래서 마지막에는 꼭 직접 눌러보는 과정이 필요합니다. 이번 편에서는 아래 순서로 확인해보는 편이 좋습니다.
- 할 일을 3개 이상 추가한 뒤 전체 보기에서 모두 보이는가
- 그중 하나를 완료 처리한 뒤 진행 중 보기에서는 미완료만 보이는가
- 완료 보기에서는 완료 항목만 보이는가
- 완료 상태를 다시 바꾸면 현재 필터에 맞게 목록이 달라지는가
- 항목을 삭제했을 때 현재 필터 기준으로도 자연스럽게 사라지는가
- 전체 삭제 뒤에는 전체 보기로 돌아오는가
- 새로고침 뒤에도 마지막 항목 상태들이 그대로 유지되는가
이 단계에서 중요한 건 "버튼 색이 바뀌었나"보다 필터 상태와 목록 상태가 같이 맞물려 움직이는가를 보는 것입니다. 필터 기능은 겉으로는 단순한데, 실제로는 상태 관리 연습에 꽤 잘 맞는 기능입니다.
핵심 포인트
필터 기능은 눈으로만 보면 쉬워 보이지만, 현재 상태가 바뀔 때 화면도 같이 맞게 움직이는지를 직접 눌러봐야 진짜 감이 옵니다.
Codex에게는 이렇게 나눠서 요청하면 된다
이번 단계도 Codex를 쓰기에 좋습니다. 다만 여전히 중요한 건 범위를 크게 던지지 않는 것입니다. 필터 기능은 HTML, CSS, JavaScript가 모두 바뀔 수 있는 작업이기 때문에, 오히려 더 조심스럽게 잘라서 요청하는 편이 좋습니다.
전체 방향을 먼저 정리받고 싶다면 이렇게 물어볼 수 있습니다.
지금 할 일 앱은 추가, 완료 체크, 삭제, 전체 삭제,
새로고침 후 유지까지 정상 동작하는 상태야.
이번에는 전체, 진행 중, 완료 필터만 추가하고 싶어.
기존 기능은 유지하고 구조도 크게 뒤집지 말아줘.
먼저 HTML, CSS, JavaScript에서
어떤 부분을 왜 바꿀지부터 설명해줘.
JavaScript 쪽만 더 좁게 요청하고 싶다면 이렇게 말할 수 있습니다.
@script.js
현재 목록 데이터는 그대로 두고,
보기 상태만 따로 저장해서
전체, 진행 중, 완료 필터가 동작하게 만들어줘.
추가, 삭제, 전체 삭제 기능은 유지해야 하고
render 흐름을 크게 망가뜨리면 안 돼.
수정 전에 새로 생길 변수와 함수부터 설명해줘.
CSS만 요청하고 싶다면 이렇게 더 작게 자를 수 있습니다.
@style.css
필터 버튼 3개를 추가할 건데,
선택된 버튼이 눈에 띄게 보이도록만 정리해줘.
전체 디자인은 크게 바꾸지 말고,
모바일에서도 버튼이 한 줄 또는 자연스럽게 줄바꿈되게 해줘.
이런 식으로 나누면 AI가 기능 추가와 전면 수정을 혼동할 가능성이 줄어듭니다. 초보자 입장에서는 이 차이가 꽤 중요합니다. 결국 AI를 잘 쓰는 것도 거창한 프롬프트보다 수정 범위를 정확하게 자르는 습관에 더 가까운 경우가 많습니다.
핵심 포인트
Codex에게 필터 기능을 맡길 때는 "필터 넣어줘"보다 무엇은 유지하고, 어디까지만 바꿀지를 같이 말하는 편이 훨씬 안정적입니다.
마무리
이번 편에서 추가한 기능은 아주 작습니다. 보기 필터 버튼 세 개가 생기고, 현재 상태에 따라 보여주는 목록이 달라지는 정도입니다. 그런데 바로 이런 기능이 프로젝트를 한 단계 더 살아 있는 도구처럼 보이게 만듭니다.
초보자에게 지금 중요한 건 거대한 기능을 한 번에 붙이는 것이 아닙니다. 기존 구조를 유지하면서 새로운 상태 하나를 추가하고, 그 상태가 화면에 어떻게 반영되는지 끝까지 따라가는 경험이 더 중요합니다. 이번 필터 기능은 그 연습으로 꽤 잘 맞습니다.
여기까지 직접 해보면, 이제 할 일 앱은 단순히 적고 지우는 수준을 조금 넘어서기 시작합니다. 같은 목록이라도 어떤 기준으로 볼지를 내가 선택할 수 있게 되었기 때문입니다. 그 차이가 생각보다 큽니다.
'바이브코딩 > 실습' 카테고리의 다른 글
| 바이브코딩 14편: 할 일 순서를 위아래로 바꾸기 (0) | 2026.04.01 |
|---|---|
| 바이브코딩 13편: 할 일 앱에 수정 기능 붙이기 (0) | 2026.03.30 |
| 바이브코딩 11편: 기능은 그대로 두고 할 일 앱 구조만 다듬기 (0) | 2026.03.25 |
| 바이브코딩 10편: 배포한 할 일 앱을 작게 개선해보기 (0) | 2026.03.23 |
| 바이브코딩 9편: 만든 할 일 앱을 인터넷에 직접 올려보기 (0) | 2026.03.20 |
