바이브코딩으로 체크리스트나 메모 앱을 만들다 보면, 저장까지는 어렵지 않게 붙는 경우가 많습니다. 새 항목도 들어가고, 새로고침해도 남습니다. 그런데 그다음부터 조금 이상해집니다. 분명 두 번째 항목을 지웠는데 다른 항목이 사라지거나, 수정했더니 엉뚱한 줄이 바뀌거나, 화면에서는 지워졌는데 새로고침하면 다시 나타나는 식입니다.

이 구간에서 초보자가 가장 많이 부딪히는 문제가 바로 “어느 항목을 정확히 바꿔야 하는가”입니다. 추가는 비교적 단순합니다. 새 항목 하나를 더하면 되니까요. 하지만 삭제와 수정은 다릅니다. 목록 안에 이미 여러 항목이 들어 있는 상태에서, 그중 하나만 정확히 집어야 합니다. 이때 필요한 개념이 id입니다.

이번 글에서는 왜 수정과 삭제가 생각보다 쉽게 꼬이는지, 왜 순서나 글자만 보고 처리하면 불안한지, 그리고 id를 붙이면 왜 흐름이 훨씬 안정되는지 차근차근 정리해보겠습니다. 처음에는 낯설어 보여도, 한 번 감을 잡고 나면 이후의 체크 완료, 필터, 정렬 같은 기능도 훨씬 덜 흔들립니다.

한눈에 보는 이번 글의 핵심

상황 왜 꼬이는가 핵심 해결책
삭제했는데 다른 항목이 사라진다 순서나 화면 위치로 대상을 찾고 있다 항목마다 고유한 id를 붙인다
같은 이름 항목이 같이 지워진다 text 값만 기준으로 찾고 있다 글자가 아니라 id로 구분한다
화면에서는 지워졌는데 새로고침하면 돌아온다 데이터는 안 바꾸고 화면만 바꿨다 배열 수정 → 저장 → 다시 그리기 순서를 지킨다
수정 후 목록이 더 꼬인다 대상 항목을 안정적으로 찾지 못한다 id를 기준으로 수정 대상을 고정한다

이번 글의 핵심 한 줄
수정과 삭제는 화면에서 하는 일이 아니라 데이터에서 하는 일이고, 그 데이터를 안정적으로 집어내는 표식이 id다.


수정과 삭제에서 왜 갑자기 복잡해질까

추가 기능은 생각보다 단순합니다. 사용자가 입력한 값을 받아서 배열 끝에 하나 더 넣고, 다시 화면에 보여주면 됩니다. 이때는 아직 “누가 누구인지”를 엄격하게 구분할 필요가 없습니다. 새로운 항목 하나만 더 생기면 되니까요.

하지만 삭제와 수정은 다릅니다. 기존 항목이 여러 개 들어 있는 상태에서, 정확히 하나만 골라내야 합니다. 이때 대상 선택 기준이 흔들리면 바로 문제가 시작됩니다. 특히 같은 이름의 할 일이 두 개 있을 수 있고, 목록 순서는 나중에 바뀔 수도 있고, 완료 여부에 따라 화면 순서가 달라질 수도 있습니다.

 

작업 겉으로 보기엔 실제로 필요한 것 자주 생기는 문제
추가 새 값 하나 넣기 입력값만 제대로 받으면 된다 빈칸 입력, 중복 허용 여부
삭제 항목 하나 없애기 어느 항목을 지울지 정확히 알아야 한다 다른 항목이 지워짐, 새로고침 후 되살아남
수정 글자만 바꾸기 대상을 정확히 찾고 새 값으로 교체해야 한다 엉뚱한 항목이 수정됨, 화면과 저장값 불일치

즉, 이 구간부터는 단순히 “배열에 값이 있다” 수준으로는 부족합니다. 배열 안의 각 항목이 누구인지를 안정적으로 구분할 수 있어야 합니다. 이 감각이 생기면 삭제와 수정이 한결 덜 불안해집니다.

추가와 수정·삭제의 차이
추가는 “새 항목 하나 더”의 문제지만, 수정과 삭제는 “기존 항목 중 정확히 누구를 건드릴 것인가”의 문제다.


index나 text만 믿으면 왜 문제가 생길까

초보자가 처음 삭제 기능을 만들 때 가장 먼저 떠올리는 기준은 보통 둘 중 하나입니다. 배열의 순서(index)를 쓰거나, 항목의 글자(text)를 그대로 기준으로 삼는 방식입니다. 처음에는 둘 다 꽤 그럴듯해 보입니다. 그런데 조금만 기능이 붙으면 금방 흔들립니다.

1. index로 찾는 방식이 왜 불안한가

배열의 순서는 지금 이 순간에는 분명합니다. 첫 번째, 두 번째, 세 번째. 그래서 아래 같은 코드가 쉽게 떠오릅니다.

todos.splice(index, 1);

문제는 화면의 순서와 데이터의 순서가 항상 같다고 장담할 수 없다는 점입니다. 나중에 완료된 항목을 아래로 보내거나, 검색 결과만 따로 보여주거나, 필터를 붙이면 화면에 보이는 순서와 실제 배열의 순서가 달라질 수 있습니다. 그 순간부터 index는 생각보다 믿기 어려운 기준이 됩니다.

2. text로 찾는 방식이 왜 불안한가

그럼 글자 자체를 기준으로 찾으면 될 것 같기도 합니다. 예를 들어 아래처럼요.

todos = todos.filter(todo => todo.text !== text);


겉보기엔 간단합니다. 그런데 같은 이름의 항목이 두 개 있으면 문제가 생깁니다. 예를 들어 장보기가 두 번 들어 있다면, 하나만 지우고 싶어도 둘 다 지워질 수 있습니다. 사용자가 보기엔 분명 첫 번째 장보기를 지운 건데, 코드 입장에서는 같은 글자를 가진 항목 전체를 지우게 되는 셈입니다.

기준 처음엔 왜 편해 보이나 실제로 생기는 문제 안정성
index 배열 순서가 눈에 보인다 정렬, 필터, 재렌더링 후 순서가 바뀌면 흔들린다 낮은 편
text 코드가 짧고 읽기 쉽다 같은 이름 항목을 구분하지 못한다 낮은 편
id 처음엔 한 단계 더 있어 보인다 항목마다 구분점이 생겨 수정과 삭제가 안정된다 높은 편

이쯤 되면 감이 옵니다. 수정과 삭제는 “지금 몇 번째 줄인지”나 “지금 무슨 글자인지”보다, 이 항목이 원래 누구인지를 기준으로 다뤄야 합니다. 바로 그 역할이 id입니다.


id는 정확히 무엇이고 왜 필요한가

id는 각 항목을 구분하기 위한 고유한 표식입니다. 중요한 건 이 값이 보기 좋을 필요는 없다는 점입니다. 사람 눈에 예쁜 이름이 아니라, 코드가 “이 항목이 바로 그 항목이구나”라고 알아보는 데 쓰는 번호표에 가깝습니다.

쉽게 비유하면 이렇습니다. 교실에서 학생을 자리 번호로만 기억하면 자리가 바뀌는 순간 헷갈립니다. 이름으로만 기억해도 동명이인이 있으면 애매해집니다. 하지만 학번이 있으면 자리가 바뀌든 이름이 비슷하든 한 사람을 정확히 구분할 수 있습니다. id도 그와 비슷합니다.

좋은 id의 조건 왜 중요한가
항목마다 달라야 한다 같은 값을 가진 항목이 둘 있으면 구분 기준이 무너진다
한 번 만들면 유지되어야 한다 수정이나 삭제 시 같은 대상을 계속 찾을 수 있다
화면 순서와 무관해야 한다 정렬이나 필터가 바뀌어도 안정적으로 항목을 찾을 수 있다
text가 바뀌어도 영향이 없어야 한다 수정 기능을 붙여도 대상이 흔들리지 않는다

입문 단계의 개인 프로젝트에서는 아래처럼 시간을 숫자로 쓰는 방식부터 시작해도 충분한 경우가 많습니다.

const todo = {

id: Date.now(),
text: '장보기',
done: false
};

Date.now()는 현재 시간을 숫자로 돌려주는 방식입니다. 간단하고, 처음 연습용으로는 꽤 쓰기 편합니다. 물론 모든 상황에서 완벽한 정답이라고 말할 수는 없습니다. 하지만 혼자 쓰는 작은 체크리스트나 메모 앱처럼 단순한 프로젝트에서는 이해하기 쉽고 적용도 빠릅니다.

여기서 중요한 점
id의 핵심은 보기 좋은 값이 아니라, 항목이 살아 있는 동안 같은 대상을 계속 가리킬 수 있느냐에 있다.


데이터 구조를 바꾸면 수정과 삭제가 쉬워진다

id를 쓰려면 데이터 구조도 조금 바뀌어야 합니다. 처음에는 아래처럼 문자열 배열로 시작하는 경우가 많습니다.

let todos = ['장보기', '운동하기', '메일 보내기'];

이 방식은 아주 단순한 목록 출력까지만 보면 나쁘지 않습니다. 하지만 수정, 삭제, 완료 여부, 생성 시각 같은 정보가 붙기 시작하면 금방 답답해집니다. 항목 하나가 단순한 글자 덩어리일 뿐이라, “누구를 수정하는지”, “완료 상태가 어떤지”를 붙이기가 애매해지기 때문입니다.

그래서 보통은 아래처럼 문자열 배열에서 객체 배열로 넘어갑니다.

let todos = [

{ id: 1710000000001, text: '장보기', done: false },
{ id: 1710000000002, text: '운동하기', done: false },
{ id: 1710000000003, text: '메일 보내기', done: true }
];
필드 역할 왜 필요한가
id 각 항목을 구분하는 번호표 수정과 삭제 대상을 안정적으로 찾기 위해
text 사용자가 보는 실제 내용 표시와 수정 대상이 되는 텍스트
done 완료 여부 체크 기능, 필터, 스타일 분리에 필요

이 구조의 장점은 분명합니다. 글자는 바뀔 수 있지만 id는 유지됩니다. 완료 여부는 따로 관리할 수 있고, 나중에 날짜나 우선순위가 필요해져도 객체 안에 자연스럽게 필드를 늘릴 수 있습니다. 즉, 삭제와 수정 때문에 시작한 구조 변경이 이후 기능 확장까지 훨씬 편하게 만들어 주는 셈입니다.

구조를 바꾼다는 것의 의미
코드를 괜히 복잡하게 만드는 게 아니라, 나중에 붙을 기능까지 버틸 수 있게 항목 하나의 정보를 정리해두는 일에 가깝다.


id로 삭제하고 id로 수정하는 기본 흐름

이제 실제 흐름을 보면 훨씬 선명합니다. 핵심은 단순합니다. 항목을 만들 때 id를 붙이고, 수정과 삭제는 그 id를 기준으로 처리하면 됩니다. 그리고 데이터가 바뀔 때마다 저장하고, 다시 화면을 그리면 됩니다.

함수 역할 핵심 포인트
addTodo() 새 항목 추가 생성 시점에 id를 한 번 만든다
deleteTodo(id) 특정 항목 삭제 id가 다른 항목만 남긴다
editTodo(id, nextText) 특정 항목 수정 id가 같은 항목만 새 글자로 바꾼다
saveTodos() 현재 배열 저장 변경 직후마다 호출한다
renderTodos() 화면 다시 그리기 화면은 항상 데이터 결과를 보여준다

아래는 개념을 이해하기 좋은 가장 단순한 예시입니다. 수정은 prompt로 처리해서 구조를 단순하게 유지했고, 삭제와 수정 모두 id를 기준으로 움직입니다.

<input id="todoInput" type="text" placeholder="할 일을 입력하세요" />

추가
    
    const STORAGE_KEY = 'vibe-todos';
    const input = document.getElementById('todoInput');
    const addBtn = document.getElementById('addBtn');
    const todoList = document.getElementById('todoList');
    
    let todos = [];
    
    function saveTodos() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
    }
    
    function loadTodos() {
    const saved = localStorage.getItem(STORAGE_KEY);
    todos = saved ? JSON.parse(saved) : [];
    }
    
    function addTodo(text) {
    const trimmed = text.trim();
    if (!trimmed) return;
    
    todos.push({
    id: Date.now(),
    text: trimmed,
    done: false
    });
    
    input.value = '';
    saveTodos();
    renderTodos();
    }
    
    function deleteTodo(id) {
    todos = todos.filter(todo => todo.id !== id);
    saveTodos();
    renderTodos();
    }
    
    function editTodo(id, nextText) {
    const trimmed = nextText.trim();
    if (!trimmed) return;
    
    todos = todos.map(todo =>
    todo.id === id
    ? { ...todo, text: trimmed }
    : todo
    );
    
    saveTodos();
    renderTodos();
    }
    
    function renderTodos() {
    todoList.innerHTML = '';
    
    todos.forEach(todo => {
    const li = document.createElement('li');
    li.textContent = todo.text + ' ';
    
    const editBtn = document.createElement('button');
    editBtn.textContent = '수정';
    
    const deleteBtn = document.createElement('button');
    deleteBtn.textContent = '삭제';
    
    editBtn.addEventListener('click', () =&gt; {
      const nextText = prompt('수정할 내용을 입력하세요', todo.text);
      if (nextText === null) return;
      editTodo(todo.id, nextText);
    });
    
    deleteBtn.addEventListener('click', () =&gt; {
      deleteTodo(todo.id);
    });
    
    li.appendChild(editBtn);
    li.appendChild(deleteBtn);
    todoList.appendChild(li);
    
    });
    }
    
    addBtn.addEventListener('click', () => {
    addTodo(input.value);
    });
    
    loadTodos();
    renderTodos();
    

    이 코드에서 꼭 봐야 할 줄은 세 군데입니다.

    1. 생성 시점
      id: Date.now()로 항목에 번호표를 붙입니다.
    2. 삭제 시점
      todo.id !== id 조건으로, 선택한 id만 제외하고 나머지를 남깁니다.
    3. 수정 시점
      todo.id === id인 항목만 새 글자로 바꾸고, 나머지는 그대로 둡니다.

    그리고 무엇보다 중요한 순서는 항상 같습니다.

    데이터를 바꾼다
    
    → 저장한다
    → 화면을 다시 그린다

    이 순서를 지키면 “화면은 바뀌었는데 저장은 안 됨”, “저장은 됐는데 화면은 그대로” 같은 어색한 상황이 크게 줄어듭니다.


    초보자가 자주 하는 실수 6가지

    id 개념을 이해해도, 실제로 붙일 때는 비슷한 곳에서 자주 꼬입니다. 아래 실수들은 초보자가 정말 자주 만나는 편입니다. 코드는 길어 보이는데 삭제나 수정이 이상하게 동작한다면, 대부분 여기 안에서 원인이 보입니다.

    실수 겉으로 보이는 증상 실제로 문제인 부분 어떻게 고치면 좋은가
    화면에서만 항목을 지운다 지워진 것처럼 보이지만 새로고침하면 돌아온다 배열 데이터는 그대로 남아 있다 배열을 먼저 바꾸고 저장한 뒤 다시 렌더링한다
    index를 그대로 넘긴다 정렬이나 필터 후 엉뚱한 항목이 지워진다 화면 순서와 실제 데이터 순서가 어긋난다 index 대신 id를 넘긴다
    text 기준으로 삭제한다 같은 이름 항목이 한꺼번에 사라진다 text는 중복될 수 있다 항목마다 고유한 id를 둔다
    수정이나 삭제 뒤 저장을 빼먹는다 그 순간엔 바뀌지만 새로고침하면 예전 상태가 나온다 saveTodos() 호출이 빠졌다 변경 직후마다 저장 함수를 호출한다
    저장은 했는데 화면을 다시 안 그린다 실제 데이터는 바뀌었는데 화면은 그대로다 renderTodos()가 빠졌다 저장 뒤 렌더링까지 한 묶음으로 본다
    렌더링할 때마다 id를 새로 만든다 같은 항목인데 매번 다른 항목처럼 취급된다 id가 안정적으로 유지되지 않는다 id는 생성 시점에 한 번만 만들고 계속 유지한다

    특히 마지막 실수는 의외로 자주 보입니다. id는 화면을 그릴 때마다 새로 만드는 값이 아닙니다. 항목이 처음 만들어질 때 한 번 붙이고, 이후에는 저장해뒀다가 계속 같은 값을 써야 합니다. 그래야 수정도 삭제도 같은 대상을 제대로 찾을 수 있습니다.

    실전에서 가장 중요한 감각
    수정과 삭제가 꼬일 때는 버튼 문제부터 의심하기보다, “내가 지금 대상을 무엇으로 찾고 있는가”를 먼저 보는 편이 훨씬 빠르다.


    마무리

    바이브코딩으로 할 일 앱이나 메모 앱을 만들 때, 저장이 붙는 순간부터 구조에 대한 감각이 필요해집니다. 추가만 할 때는 잘 안 보이던 문제가 수정과 삭제에서 한꺼번에 드러나는 이유도 여기에 있습니다. 결국 핵심은 하나입니다. 각 항목을 안정적으로 구분할 수 있어야 한다는 점입니다.

    이번 글에서 꼭 가져가면 좋은 건 세 가지입니다.
    첫째, index나 text만으로는 수정과 삭제가 쉽게 흔들릴 수 있다는 점.
    둘째, id는 보기 좋은 값이 아니라 각 항목을 구분하는 번호표라는 점.
    셋째, 수정과 삭제는 화면에서가 아니라 데이터에서 먼저 처리하고, 그다음 저장과 렌더링이 따라와야 한다는 점입니다.


    이 흐름이 잡히면 이제 체크 완료 기능도 훨씬 다루기 쉬워집니다. 다음 편에서는 done 상태를 객체 안에서 함께 관리하는 방식, 그리고 완료/미완료를 나누기 시작할 때 왜 렌더링 구조가 더 중요해지는지를 이어서 정리하겠습니다.