배포까지 한 번 끝내고 나면, 그다음에는 묘하게 손이 커지기 쉽습니다. 여기까지 왔으니 필터도 넣고, 카테고리도 넣고, 날짜도 붙이고 싶어지죠. 그런데 초보자일수록 이 시점에서는 오히려 반대로 가는 편이 좋습니다. 큰 기능 추가보다 작은 개선을 분명하게 붙이는 것이 훨씬 중요합니다.

왜냐하면 지금 단계에서 정말 필요한 건 "뭔가 더 대단한 앱"이 아니라, 이미 돌아가는 프로젝트를 안전하게 다듬는 경험이기 때문입니다. 배포까지 끝낸 프로젝트를 다시 열어보고, 불편한 지점을 하나씩 줄이고, 변경 전후를 비교하고, 다시 반영하는 흐름이 쌓여야 다음 프로젝트도 덜 흔들립니다.

이번 글에서는 지난 편에서 배포한 할 일 앱을 기준으로, 눈에 보이는 개선을 세 가지만 붙여보겠습니다. 범위는 일부러 작게 잡겠습니다. 그래야 "작은 개선을 정확하게 넣는 감각"이 남기 때문입니다.

이번 편에서 바꾸는 것과 바꾸지 않는 것
이번에 추가하는 것 이번에는 일부러 안 하는 것 왜 이렇게 자르나
남은 할 일 개수 표시 카테고리 분류 지금은 정보 하나를 더 잘 보여주는 연습이 먼저입니다
전체 삭제 버튼 마감일, 정렬, 필터 확장 기능을 많이 늘리기보다 작은 불편을 줄이는 편이 더 중요합니다
상태 문구 다듬기 구조 전체 리팩터링 이번 편은 기능을 갈아엎는 글이 아니라 사용감을 조금 더 낫게 만드는 글입니다

이번 글의 핵심 한 줄
배포 다음 단계에서 중요한 건 "더 많은 기능"보다 지금 돌아가는 앱을 조금 더 읽기 쉽고, 누르기 쉽고, 실수하기 덜한 상태로 만드는 것입니다.


왜 배포 다음에는 크게 뜯지 말고 작게 고쳐야 하는가

처음 만든 프로젝트를 배포까지 해봤다면, 이제부터는 "만드는 단계"와 "다듬는 단계"를 조금 구분해서 보는 편이 좋습니다. 초보자는 보통 기능을 더 넣는 쪽만 성취처럼 느끼기 쉬운데, 실제로는 불편한 지점을 줄이고, 위험한 동작을 안전하게 바꾸고, 상태를 더 잘 보이게 만드는 일도 아주 중요합니다.

예를 들어 지금까지 만든 할 일 앱은 기본 기능은 이미 있습니다. 추가, 완료 체크, 삭제, 새로고침 후 유지까지 되니까, 앱으로서 아주 기초적인 역할은 해내고 있는 셈입니다. 그런데 막상 직접 써보면 이런 아쉬움이 생깁니다.

  • 지금 남은 할 일이 몇 개인지 한눈에 안 보입니다.
  • 항목이 많아졌을 때 한 번에 비우는 동작이 없습니다.
  • 아무것도 없을 때와 뭔가 저장된 뒤의 안내 문구가 조금 더 분명했으면 좋겠습니다.

이런 개선은 겉으로 보기엔 작아 보여도, 실제로는 꽤 좋은 연습입니다. 구조를 완전히 뒤집지 않으면서도 사용자 입장에서 느껴지는 차이를 만들 수 있기 때문입니다.

핵심 포인트
배포 다음 단계에서 중요한 건 "더 큰 기능"보다 작은 불편을 정확하게 줄이는 경험입니다.


이번 편에서 개선할 것 3가지

이번 편에서는 욕심내지 않고 딱 세 가지만 하겠습니다.

  • 남은 할 일 개수 표시 : 지금 해야 할 일이 몇 개인지 바로 보이게 합니다.
  • 전체 삭제 버튼 : 하나씩 지우지 않고도 전체를 비울 수 있게 합니다.
  • 안내 문구 정리 : 비어 있을 때, 추가했을 때, 취소했을 때 메시지가 조금 더 분명하게 보이도록 바꿉니다.

여기서 중요한 건 세 기능을 묶어서 보는 방식입니다. 각각 따로 보면 사소해 보이지만, 합치면 앱의 사용감이 꽤 달라집니다. 그리고 이 정도 범위면 HTML, CSS, JavaScript가 각각 어떻게 연결되는지 다시 보기에도 좋습니다.

이번에도 기준은 단순합니다. 기존 구조를 완전히 뒤엎지 않는다, 그리고 지금 잘 되던 기능은 유지한다입니다. 초보자 실습에서는 이 두 가지가 꽤 중요합니다.


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">
      <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"
            placeholder="예: 배포 버전 점검하기"
          >
          <button type="submit">추가</button>
        </div>
      </form>

      <section class="toolbar" aria-label="할 일 요약 및 관리">
        <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>
</body>
</html>

이번 HTML 수정에서 핵심은 딱 두 군데입니다.

  • summaryText : 남은 할 일 개수를 보여줄 자리입니다.
  • clearAllButton : 전체 삭제를 실행할 버튼입니다.

즉, 이번에는 새로운 입력창을 더 만드는 게 아니라, 현재 상태를 보여주는 정보와 관리 버튼을 추가하는 쪽으로 구조를 보강한 셈입니다. 이 정도 수정은 기존 앱을 크게 흔들지 않으면서도 꽤 실용적입니다.

핵심 포인트
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);
}

.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 0 24px;
  line-height: 1.7;
  color: #374151;
}

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;
}

button:not(.secondary-button):not(.delete-button) {
  border: none;
  border-radius: 14px;
  padding: 14px 18px;
  font-weight: 700;
  cursor: pointer;
  background: #111827;
  color: #ffffff;
}

.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;
}

.secondary-button {
  border-radius: 14px;
  padding: 14px 18px;
  font-weight: 700;
  cursor: pointer;
  background: #ffffff;
  color: #111827;
  border: 1px solid #d1d5db;
}

.secondary-button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.message {
  margin: 14px 0 0;
  line-height: 1.6;
  color: #6b7280;
}

.todo-list {
  list-style: none;
  margin: 24px 0 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 {
  border-radius: 14px;
  padding: 14px 18px;
  font-weight: 700;
  cursor: pointer;
  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;
  }

  .todo-item {
    flex-direction: column;
    align-items: stretch;
  }
}

이번 CSS에서 봐야 할 포인트도 많지 않습니다.

  • toolbar : 남은 개수와 전체 삭제 버튼을 한 줄에 묶습니다.
  • summary : 상태 문구가 제목처럼 읽히도록 조금 더 또렷하게 보이게 합니다.
  • secondary-button : 주 버튼과 성격이 다른 보조 버튼처럼 보이게 만듭니다.
  • disabled 상태 : 비활성 버튼이 눌릴 것처럼 보이지 않게 만듭니다.

이런 연결이 보이면 CSS도 갑자기 덜 뿌옇게 느껴집니다. 코드 양보다 "어느 부분을 보기 좋게 만들려는지"가 먼저 보이기 시작하기 때문입니다.

핵심 포인트
이번 CSS는 "더 예쁘게"보다 상태와 역할이 더 잘 읽히게 만드는 쪽에 가깝습니다.


JavaScript 수정: 남은 개수, 전체 삭제 확인, 안내 문구 반영

이번 편의 핵심은 JavaScript입니다. 실제 개선이 체감되는 부분이 거의 여기서 일어나기 때문입니다. 다만 이번에도 원칙은 같습니다. 기존 흐름을 완전히 바꾸는 게 아니라, 지금 있던 배열과 렌더링 구조 위에 작은 기능을 더하는 방식으로 가겠습니다.

이번 JavaScript에서 먼저 봐야 할 것
이름 역할 왜 중요한가
updateSummary 남은 개수와 버튼 상태를 갱신합니다 이번 편의 가장 눈에 띄는 개선이 여기서 생깁니다
clearAllButton 이벤트 전체 삭제 전 확인을 받고 처리합니다 실수로 한 번에 지우는 위험을 줄입니다
renderTodos(messageText) 상황에 따라 문구를 다르게 보여줍니다 추가, 삭제, 취소 뒤의 상태가 더 분명해집니다

script.js는 아래처럼 정리해보겠습니다.

const STORAGE_KEY = "vibe-todos";

const todoForm = document.querySelector("#todoForm");
const todoInput = document.querySelector("#todoInput");
const todoList = document.querySelector("#todoList");
const message = document.querySelector("#message");
const summaryText = document.querySelector("#summaryText");
const clearAllButton = document.querySelector("#clearAllButton");

let todos = loadTodos();

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 showMessage(text) {
  message.textContent = text;
}

function updateSummary() {
  const remainingCount = todos.filter(function (todo) {
    return !todo.done;
  }).length;

  summaryText.textContent = "남은 할 일 " + remainingCount + "개";
  clearAllButton.disabled = todos.length === 0;
}

function createTodoItem(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);

  return item;
}

function renderTodos(messageText) {
  todoList.innerHTML = "";
  updateSummary();

  if (todos.length === 0) {
    showMessage(messageText || "아직 등록된 할 일이 없습니다.");
    return;
  }

  todos.forEach(function (todo) {
    todoList.appendChild(
      createTodoItem(todo)
    );
  });

  showMessage(
    messageText ||
      "체크해서 완료 처리하거나, 필요 없으면 삭제해보세요."
  );
}

todoForm.addEventListener("submit", function (event) {
  event.preventDefault();

  const text = todoInput.value.trim();

  if (!text) {
    showMessage("할 일을 먼저 입력해보세요.");
    todoInput.focus();
    return;
  }

  todos.unshift({
    id: Date.now(),
    text: text,
    done: false
  });

  todoInput.value = "";
  saveTodos();
  renderTodos("할 일을 추가했습니다.");
  todoInput.focus();
});

clearAllButton.addEventListener("click", function () {
  if (todos.length === 0) {
    return;
  }

  const shouldClear = window.confirm(
    "정말 모든 할 일을 삭제하시겠습니까?"
  );

  if (!shouldClear) {
    showMessage("전체 삭제를 취소했습니다.");
    return;
  }

  todos = [];
  saveTodos();
  renderTodos("모든 항목을 삭제했습니다.");
});

renderTodos();

이 코드를 읽을 때는 한 줄씩 전부 붙잡기보다, 이번 편에서 새로 생긴 역할을 먼저 보면 됩니다.

1) 남은 개수를 따로 계산합니다

updateSummary가 이번 편의 첫 번째 핵심입니다. 여기서는 완료되지 않은 항목만 세어서 상단 문구에 보여줍니다. 즉, 사용자가 목록을 볼 때마다 "지금 실제로 남은 일이 몇 개인가"를 바로 읽을 수 있게 됩니다.

2) 전체 삭제 버튼은 아무 때나 활성화되지 않습니다

목록이 하나도 없을 때 전체 삭제 버튼이 계속 살아 있으면, 사용자 입장에서는 조금 이상합니다. 그래서 항목이 없을 때는 버튼을 비활성화해두는 쪽이 자연스럽습니다. 이건 작은 차이 같지만, 실제 사용감은 꽤 좋아집니다.

3) 전체 삭제 전에 한 번 더 묻습니다

이번 편의 두 번째 핵심은 확인창입니다. 삭제는 되돌리기 전에 한 번쯤 멈춰보게 하는 편이 좋습니다. 특히 전체 삭제처럼 영향이 큰 동작은 더 그렇습니다. 그래서 버튼을 누르자마자 바로 비우지 않고, 한 번 더 확인하는 흐름을 넣었습니다.

4) 안내 문구도 상태에 따라 조금씩 바뀝니다

지금까지는 목록만 바뀌는 쪽에 더 집중했다면, 이번에는 사용자에게 "무슨 일이 일어났는지"를 조금 더 분명하게 보여주도록 바꿨습니다. 추가했을 때, 삭제했을 때, 전체 삭제를 취소했을 때 문구가 조금씩 다르게 보이도록 한 이유가 여기 있습니다.

전체 흐름을 짧게 줄이면 이렇게 볼 수 있습니다.

배열 상태를 바꾼다
-> 저장한다
-> 남은 개수를 다시 계산한다
-> 목록을 다시 그린다
-> 현재 상황에 맞는 문구를 보여준다

핵심 포인트
이번 편의 JavaScript 핵심은 새로운 문법보다 기존 흐름을 유지하면서 상태 표시를 더 분명하게 만든다는 데 있습니다.


직접 눌러보며 테스트하기

작은 개선은 얼핏 보면 쉬워 보여서, 오히려 확인을 대충 넘기기 쉽습니다. 그런데 이번 편처럼 "상태를 더 잘 보이게 만드는 기능"은 직접 눌러보지 않으면 어색한 부분을 놓치기 쉽습니다.

이번 앱이라면 아래 정도만 확인해도 충분합니다.

  • 할 일을 두세 개 추가했을 때 남은 개수가 맞게 보이는가
  • 체크를 하면 남은 개수가 줄어드는가
  • 체크를 다시 풀면 남은 개수가 늘어나는가
  • 전체 삭제 버튼이 목록이 있을 때만 활성화되는가
  • 전체 삭제를 눌렀을 때 바로 지워지지 않고 한 번 더 묻는가
  • 전체 삭제를 취소하면 목록이 그대로 남는가
  • 전체 삭제 후 새로고침해도 비어 있는 상태가 유지되는가

이런 확인이 중요한 이유는, AI가 코드를 잘 만들어줬는지보다 내가 지금 이 기능을 어떤 기준으로 검증하는지를 익히는 데 더 도움이 되기 때문입니다.

핵심 포인트
작은 개선일수록 "어디가 편해졌는지"를 직접 눌러봐야 체감이 남습니다.


Git 체크포인트까지 남겨보기

이번 수정이 마음에 들고 기본 동작도 다시 확인됐다면, 이제는 체크포인트를 남기고 배포 버전에도 반영하면 됩니다. 여기서도 순서는 단순합니다. 먼저 로컬 상태를 확인하고, 기록을 남깁니다.

git status
git add .
git commit -m "할 일 앱 요약과 전체 삭제 기능 추가"

이번 커밋 메시지도 꽤 중요합니다. 단순히 "수정"이라고 적기보다, 무엇이 달라졌는지가 보여야 나중에 기록을 다시 볼 때 훨씬 편합니다.

그리고 아직 로컬에서만 작업 중인 상태라면, 이후 배포할 때 이 버전이 기준점이 됩니다. 즉, "지금 잘 되는 개선 버전"을 하나 남겨두는 셈입니다.

핵심 포인트
작은 개선도 잘 됐다면 커밋으로 남기는 편이 좋습니다. 나중에 봐도 "언제 사용감이 좋아졌는지"를 기록으로 다시 읽을 수 있기 때문입니다.


Codex에게는 이렇게 나눠서 요청하면 된다

이번 단계에서도 Codex를 쓸 수 있습니다. 다만 여전히 중요한 건 한 번에 다 시키지 않는 겁니다. 특히 지금처럼 이미 배포한 프로젝트는 더 그렇습니다. 큰 변경보다 작고 분명한 수정 요청이 훨씬 안정적입니다.

처음 요청은 아래 정도면 충분합니다.

지금 배포한 할 일 앱을 작게 개선하고 싶어.
기능은 남은 할 일 개수 표시, 전체 삭제 버튼, 안내 문구 정리만 넣어줘.
HTML, CSS, JavaScript를 모두 바꿀 수는 있지만 구조는 크게 뒤집지 말아줘.
먼저 어떤 파일을 왜 바꿀지부터 설명해줘.

이 요청이 좋은 이유는 범위가 작고, 바꾸지 말아야 할 기준도 같이 들어 있기 때문입니다. 초보자가 가장 자주 놓치는 부분도 여기입니다. "무엇을 넣을지"만 말하고, "어디까지는 건드리지 말아야 하는지"를 같이 주지 않는 경우가 많습니다.

이미 기본 개선이 들어간 뒤, 한 부분만 더 다듬고 싶다면 이렇게 더 좁게 물을 수 있습니다.

@script.js
전체 삭제 버튼을 누르면 바로 지우지 말고 확인창을 먼저 띄워줘.
취소했을 때는 현재 목록이 그대로 유지되게 해줘.
수정 범위는 필요한 부분만 최소한으로 잡아줘.

또는 안내 문구만 더 자연스럽게 바꾸고 싶다면 이렇게 나눌 수 있습니다.

@script.js
현재 기능은 그대로 두고 안내 문구만 조금 더 자연스럽게 다듬어줘.
문구가 바뀌는 지점이 어디인지 먼저 짧게 설명한 뒤 수정해줘.

이렇게 요청을 작게 쪼개면, AI가 결과를 넓게 흔들 가능성도 줄고, 초보자가 무엇이 바뀌었는지 따라가기도 훨씬 쉬워집니다.

핵심 포인트
Codex를 잘 쓰는 핵심은 긴 설명보다 수정 범위를 작게 자르는 것에 더 가깝습니다.


마무리

이번 편에서 한 일은 거창하지 않습니다. 남은 개수를 보여주고, 전체 삭제를 더 안전하게 만들고, 문구를 조금 더 분명하게 다듬었을 뿐입니다. 그런데 바로 이런 수정이 프로젝트를 한 단계 더 "사용 가능한 상태"로 만듭니다.

초보자에게 지금 중요한 건 커다란 기능을 무리해서 붙이는 것이 아닙니다. 이미 돌아가는 프로젝트를 조금 더 읽기 좋고, 누르기 좋고, 실수하기 덜한 상태로 다듬는 것이 더 중요합니다. 이 감각이 붙어야 다음 프로젝트도 덜 급해지고, AI에게도 훨씬 또렷하게 요청할 수 있습니다.

여기까지 직접 해보면 왜 "배포 다음 단계"가 중요한지도 감이 옵니다. 새로운 기능을 많이 붙이지 않아도, 이미 있는 프로젝트를 조금 더 나은 상태로 만들 수 있다는 걸 직접 느끼게 되기 때문입니다. 그 경험이 쌓일수록 앱을 보는 눈도 조금씩 달라집니다.