체크리스트 앱을 처음 만들 때는 보통 속도가 꽤 빠릅니다. 입력창 하나 만들고, 추가 버튼 붙이고, 목록을 그리고, 삭제를 붙입니다. 조금 더 가면 저장도 넣고 완료 체크도 붙입니다. 여기까지는 “생각보다 잘 되네” 싶은 구간이 이어집니다. 그런데 어느 순간부터 코드가 갑자기 무거워집니다. 한 부분을 고치면 다른 부분이 깨지고, AI에게 수정을 요청할수록 함수 하나가 점점 길어지고, 나중에는 어디를 건드려야 할지 감이 잘 안 잡힙니다.
이건 초보자만 겪는 문제가 아닙니다. 오히려 바이브코딩에서는 더 빨리 드러나는 편입니다. 이유는 단순합니다. 직접 설계부터 천천히 쌓기보다, 작동하는 조각을 빠르게 붙여 가는 흐름이 많기 때문입니다. 처음엔 편합니다. 하지만 추가, 삭제, 저장, 렌더링, 필터, 완료 상태가 한 함수 안에 엉키기 시작하면, 그때부터는 코드가 “길다”보다 역할이 섞였다는 쪽이 더 큰 문제가 됩니다.
그래서 이번 단계에서 필요한 건 어려운 문법이 아니라 함수를 어떤 기준으로 나눌지에 대한 감각입니다. 보기 좋게 꾸미기 위한 정리가 아니라, 어디가 데이터를 바꾸는 곳이고 어디가 저장하는 곳인지, 어디가 화면을 그리는 곳인지 구분하는 감각이 있어야 이후 수정도 덜 흔들립니다.
| 겉으로 보이는 문제 | 실제로 일어나는 일 | 가장 먼저 손봐야 할 방향 |
|---|---|---|
| 함수 하나가 너무 길어졌다 | 한 함수가 여러 역할을 동시에 맡고 있다 | 역할 기준으로 나눠 본다 |
| 한 부분 수정하면 다른 부분이 깨진다 | 저장, 렌더링, 데이터 변경이 강하게 엉켜 있다 | 데이터 처리와 화면 처리를 분리한다 |
| AI가 고칠수록 코드가 더 길어진다 | 수정 범위를 좁힐 기준이 부족하다 | 기능 단위가 아니라 역할 단위로 함수를 세운다 |
| 오류가 났을 때 어디부터 봐야 할지 모르겠다 | 문제 구간을 좁힐 수 있는 구조가 없다 | 저장, 렌더링, 이벤트를 분리해 찾는다 |
이번 글의 핵심 한 줄
함수 분리는 코드를 예쁘게 보이게 하려는 습관이 아니라, 어떤 부분이 무슨 책임을 맡고 있는지 분명하게 만드는 작업에 가깝다.
왜 기능이 늘면 코드가 갑자기 지저분해질까
처음에는 함수가 길어져도 크게 문제 없어 보입니다. 어차피 기능이 몇 개 없고, 눈으로도 어느 정도 따라갈 수 있기 때문입니다. 예를 들어 추가 버튼을 눌렀을 때 입력값을 읽고, 배열에 넣고, 저장하고, 화면을 다시 그리는 정도는 한 함수 안에 있어도 돌아갑니다.
문제는 기능이 늘어난 다음입니다. 같은 함수 안에 아래 같은 일들이 한꺼번에 들어오기 시작합니다.
- 입력값이 비었는지 검사한다
- 새 항목 객체를 만든다
- 배열에 넣는다
- localStorage에 저장한다
- 목록을 다시 그린다
- 남은 할 일 개수를 갱신한다
- 필터 상태에 따라 화면을 다시 정리한다
- 안내 문구를 보여준다
이 시점부터는 함수가 길어서 힘든 게 아니라, 서로 다른 종류의 일이 한곳에 모여 있어서 힘들어집니다. 저장 문제인지, 렌더링 문제인지, 입력 검사 문제인지가 한눈에 안 보입니다. 그래서 AI에게 “여기만 조금 수정해줘”라고 해도 실제로는 아주 넓은 범위를 함께 건드리게 되기 쉽습니다.
| 상태 | 초보자 눈에는 | 실제로는 |
|---|---|---|
| 기능이 적을 때 | 한 함수로도 대충 버틸 만하다 | 역할이 섞여 있어도 아직 규모가 작다 |
| 기능이 늘기 시작할 때 | 점점 길어지고 복잡해 보인다 | 책임이 여러 방향으로 늘어난다 |
| 수정이 반복될 때 | 고칠수록 더 지저분해진다 | 함수 경계가 없어서 덧붙이기식 수정이 이어진다 |
중요한 포인트
코드가 지저분해지는 핵심 원인은 길이 자체보다, 한 함수가 너무 많은 결정을 대신하고 있다는 점에 있는 경우가 많다.
함수 분리는 예쁘게 꾸미는 일이 아니다
초보자가 함수 분리 이야기를 들으면 종종 이렇게 받아들입니다. “아직 기능도 다 못 만들었는데 벌써 코드 정리부터 해야 하나?” 그런데 여기서 말하는 함수 분리는 꾸미기와는 조금 다릅니다. 들여쓰기나 줄 수를 예쁘게 맞추는 것이 아니라, 역할을 분명하게 나누는 것에 가깝습니다.
예를 들어 체크리스트 앱이라면 아래 같은 역할이 있습니다.
| 역할 | 무슨 일을 하는가 | 예시 함수명 |
|---|---|---|
| 데이터 변경 | 할 일을 추가하거나 수정하거나 삭제한다 | addTodo, editTodo, deleteTodo |
| 저장 | 현재 데이터를 브라우저에 기록한다 | saveTodos, loadTodos |
| 화면 표시 | 지금 데이터 상태를 기준으로 목록을 그린다 | renderTodos, renderCount |
| 이벤트 연결 | 버튼 클릭이나 체크 변경에 반응하게 만든다 | bindEvents, handleAddClick |
이렇게 나누는 이유는 코드를 멋있게 보이게 하려는 것이 아닙니다. 문제가 생겼을 때 어느 역할 구간을 먼저 봐야 하는지가 훨씬 빨리 보이기 때문입니다. 새로고침 후 데이터가 안 남는다면 저장 구간을 먼저 보면 되고, 체크는 됐는데 취소선이 안 보이면 렌더링 구간을 먼저 보면 됩니다.
즉, 함수 분리는 개발자 취향 문제가 아니라, 문제를 좁혀서 볼 수 있게 만드는 장치에 가깝습니다. 바이브코딩에서 특히 중요한 이유도 여기에 있습니다. AI가 코드를 빨리 만들어주는 만큼, 사람이 어디를 봐야 할지 구조를 알아야 덜 휘청이기 때문입니다.
이렇게 이해하면 쉽다
이벤트는 신호를 받고, 데이터 함수는 값을 바꾸고, 저장 함수는 기록하고, 렌더링 함수는 그 결과를 보여준다. 이 네 가지가 뒤섞이기 시작하면 코드가 빠르게 무거워진다.
초보자가 먼저 나누면 좋은 역할 4가지
처음부터 모든 함수를 잘게 쪼갤 필요는 없습니다. 오히려 너무 잘게 나누면 입문자에게는 흐름이 더 안 보일 수 있습니다. 그래서 처음에는 딱 네 가지 역할만 분리해도 충분한 경우가 많습니다.
1. 데이터 바꾸는 함수
이 함수는 실제 목록을 바꾸는 데 집중합니다. 할 일을 추가하거나, 특정 항목의 완료 상태를 바꾸거나, 삭제하는 식입니다. 중요한 건 이 함수가 무엇을 보여줄지보다 무엇이 바뀌는지에 집중해야 한다는 점입니다.
2. 저장하는 함수
저장은 보통 눈에 안 보이기 때문에 자주 잊힙니다. 그래서 아예 따로 빼는 편이 좋습니다. 저장 함수가 분리되어 있으면 “바뀐 데이터가 저장까지 갔는가”를 따로 확인하기 쉬워집니다.
3. 화면을 그리는 함수
렌더링 함수는 현재 데이터 상태를 기준으로 화면을 다시 만드는 역할을 맡습니다. 여기서는 데이터를 새로 만들기보다, 이미 있는 데이터를 화면으로 보여주는 데 집중하는 편이 좋습니다.
4. 이벤트를 받는 함수
버튼 클릭, 체크박스 변경, 엔터 입력 같은 것은 이벤트 구간입니다. 이 부분은 직접 일을 끝내기보다, “어떤 함수로 넘길지”를 연결하는 역할을 맡는 편이 훨씬 덜 꼬입니다.
| 먼저 나눌 역할 | 질문으로 바꿔 보면 | 이 역할에 섞이면 곤란한 것 |
|---|---|---|
| 데이터 변경 | 실제로 어떤 값이 바뀌는가 | 화면 장식, 안내 문구, 이벤트 연결 |
| 저장 | 지금 상태를 어디에 남길 것인가 | 화면 그리기, 입력값 검사 |
| 렌더링 | 현재 데이터를 어떻게 보여줄 것인가 | 데이터 생성, 저장 로직 |
| 이벤트 | 사용자의 행동을 어떤 흐름에 연결할 것인가 | 모든 실제 처리 로직을 한곳에 다 쓰는 것 |
입문자에게는 이 정도 기준만 있어도 큰 도움이 됩니다. 기능이 늘어나도 “지금 내가 고치려는 건 저장 문제인가, 렌더링 문제인가”를 구분할 수 있게 되기 때문입니다.
한 함수에 몰아넣은 코드와 나눠진 코드의 차이
설명만 보면 감이 잘 안 올 수 있습니다. 그래서 아주 단순한 예시로 비교해보면 차이가 분명해집니다. 아래 코드는 추가 버튼 클릭 시 일어나는 일을 한 함수에 모두 넣은 형태입니다.
function handleAddClick() {
const text = input.value.trim();
if (!text) {
message.textContent = '할 일을 입력하세요';
return;
}
todos.push({
id: Date.now(),
text: text,
done: false
});
localStorage.setItem('todos', JSON.stringify(todos));
input.value = '';
todoList.innerHTML = '';
todos.forEach(todo => {
const li = document.createElement('li');
li.textContent = todo.text;
todoList.appendChild(li);
});
count.textContent = '남은 할 일: ' + todos.filter(todo => !todo.done).length;
message.textContent = '';
}
이 코드는 돌아갈 수는 있습니다. 하지만 역할이 너무 많이 섞여 있습니다. 입력 검사, 데이터 추가, 저장, 목록 렌더링, 개수 표시, 메시지 처리까지 전부 한곳에 몰려 있습니다. 이 상태에서 “저장만 조금 바꾸고 싶다”거나 “렌더링만 손보고 싶다”는 요청은 생각보다 까다로워집니다.
같은 흐름을 역할에 맞게 나누면 보통 아래처럼 정리됩니다.
function addTodo(text) {
const trimmed = text.trim();
if (!trimmed) return false;
todos.push({
id: Date.now(),
text: trimmed,
done: false
});
return true;
}
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
}
function renderTodos() {
todoList.innerHTML = '';
todos.forEach(todo => {
const li = document.createElement('li');
li.textContent = todo.text;
todoList.appendChild(li);
});
}
function renderCount() {
count.textContent = '남은 할 일: ' + todos.filter(todo => !todo.done).length;
}
function handleAddClick() {
const added = addTodo(input.value);
if (!added) {
message.textContent = '할 일을 입력하세요';
return;
}
input.value = '';
message.textContent = '';
saveTodos();
renderTodos();
renderCount();
}
이 구조가 특별히 더 고급이라서 좋은 것은 아닙니다. 다만 문제가 생겼을 때 어디를 먼저 볼지 훨씬 분명해집니다.
| 문제 상황 | 한 함수에 몰린 구조에서는 | 나눠진 구조에서는 |
|---|---|---|
| 저장이 안 된다 | 전체 흐름을 다시 읽어야 한다 | saveTodos부터 보면 된다 |
| 목록이 안 보인다 | 추가 로직과 렌더링이 섞여 있어 헷갈린다 | renderTodos를 먼저 보면 된다 |
| 빈 입력 처리만 바꾸고 싶다 | 클릭 함수 전체를 손대기 쉽다 | addTodo 안의 입력 검사만 보면 된다 |
| 남은 개수 표시가 틀린다 | 다른 기능을 건드리다 같이 흔들릴 수 있다 | renderCount를 따로 확인할 수 있다 |
실전 감각으로 정리하면
한 함수에 몰린 구조는 처음엔 빠르지만, 수정이 반복될수록 불안해진다. 반대로 역할을 나눈 구조는 처음엔 조금 더 길어 보여도, 이후 수정과 디버깅이 훨씬 쉬워진다.
바이브코딩에서 함수 분리가 특히 중요한 이유
이 부분은 일반적인 코딩보다 바이브코딩에서 더 중요하게 느껴집니다. 이유는 분명합니다. AI에게 수정을 요청할 때도, 코드 안의 역할이 분리돼 있을수록 요청 범위를 좁히기 쉽기 때문입니다.
예를 들어 코드가 하나의 큰 함수에 몰려 있다면, 아래처럼 말하기 쉽습니다.
“전체 코드를 좀 더 깔끔하게 고쳐줘.”
이 요청은 너무 넓습니다. AI는 저장 방식, 렌더링 방식, 메시지 처리까지 전부 건드릴 수 있습니다. 그러면 분명 잘되던 부분까지 흔들릴 가능성이 커집니다.
반대로 함수가 어느 정도 나뉘어 있으면 요청도 이렇게 바뀝니다.
“renderTodos는 유지하고, addTodo의 빈 입력 처리만 바꿔줘.”
“저장 로직은 그대로 두고, 남은 개수 표시 함수만 수정해줘.”
“이벤트 연결 방식은 건드리지 말고, 렌더링 쪽만 정리해줘.”
이 차이는 큽니다. 바이브코딩에서는 AI가 코드를 써주는 속도만큼, 사람이 수정 범위를 얼마나 정확히 지정할 수 있는가도 중요하기 때문입니다. 결국 함수 분리는 코드 관리의 문제이기도 하지만, 동시에 AI와 협업할 때 통제력을 유지하는 방법이기도 합니다.
| 구조 | AI에게 요청할 때 생기는 문제 | 체감 결과 |
|---|---|---|
| 한 함수에 몰린 구조 | 수정 범위를 좁히기 어렵다 | 잘되던 부분까지 흔들리기 쉽다 |
| 역할이 나뉜 구조 | 원하는 구간만 집어서 요청하기 쉽다 | 수정 품질이 비교적 안정된다 |
바이브코딩 관점의 핵심
함수 분리는 사람이 코드를 더 잘 읽기 위해서만이 아니라, AI에게도 더 정확한 수정 범위를 전달하기 위해 필요하다.
초보자가 자주 하는 실수 6가지
함수 분리를 시작할 때도 비슷한 실수가 반복됩니다. 너무 대충 섞거나, 반대로 너무 잘게 쪼개거나, 이름을 애매하게 붙이는 식입니다. 아래 실수들은 초보자가 실제로 자주 겪는 편입니다.
| 실수 | 겉으로 보이는 증상 | 왜 문제가 되는가 | 어떻게 보는 게 좋은가 |
|---|---|---|---|
| 이벤트 함수 안에 모든 로직을 다 쓴다 | 클릭 함수가 끝없이 길어진다 | 이벤트 처리와 실제 작업이 섞인다 | 이벤트는 흐름을 시작하는 역할까지만 맡긴다 |
| 렌더링 함수가 데이터를 바꾼다 | 화면을 그릴 때마다 값이 예상과 다르게 변한다 | 보여주기와 데이터 변경이 뒤섞인다 | 렌더링은 보여주는 역할에 가깝게 둔다 |
| 저장 로직을 여러 함수에 반복해서 쓴다 | 어느 곳은 저장되고 어느 곳은 안 된다 | 변경 시 수정해야 할 지점이 흩어진다 | 저장은 가능한 한 한 함수로 모은다 |
| 함수 이름이 너무 모호하다 | 나중에 다시 볼 때 역할이 안 보인다 | doIt, process, handleData 같은 이름은 범위가 너무 넓다 | 무엇을 하는지 드러나는 이름을 붙인다 |
| 너무 잘게 쪼개서 흐름이 끊긴다 | 오히려 어디서 시작되는지 더 안 보인다 | 초보자에게는 지나친 분리가 부담이 될 수 있다 | 처음엔 네 가지 역할 정도만 나눠도 충분하다 |
| 같은 역할의 코드가 여러 곳에 흩어진다 | 수정할 때 한 군데만 고쳐서 버그가 남는다 | 역할 경계가 없으면 일관성이 무너진다 | 저장, 렌더링, 데이터 변경을 가능한 한 모은다 |
여기서 특히 마지막 두 가지를 같이 기억해두면 좋습니다. 안 나누는 것도 문제지만, 과하게 나누는 것도 문제가 될 수 있습니다. 초보자에게 중요한 건 디자인 패턴 이름을 외우는 것이 아니라, 지금 이 함수가 무슨 책임을 맡고 있는지 말로 설명할 수 있느냐입니다.
가장 현실적인 기준
함수를 봤을 때 “이건 저장하는 곳”, “이건 화면 그리는 곳”, “이건 클릭을 받는 곳” 정도가 바로 떠오르면 이미 꽤 좋은 분리다.
'바이브코딩 > 이론' 카테고리의 다른 글
| 10. 정렬을 붙였더니 왜 원래 순서가 사라질까: 바이브코딩에서 sort가 헷갈리는 이유 (0) | 2026.03.17 |
|---|---|
| 9. 검색을 붙였더니 왜 목록이 꼬일까: 바이브코딩에서 원본 데이터 지키는 법 (0) | 2026.03.15 |
| 7. 완료 체크를 붙이면 왜 갑자기 꼬일까: 바이브코딩에서 done 상태 이해하기 (0) | 2026.03.13 |
| 6. 삭제는 되는데 왜 다른 항목이 지워질까: 바이브코딩에서 id가 필요한 이유 (0) | 2026.03.12 |
| 5. 왜 새로고침하면 데이터가 사라질까: 바이브코딩 초보를 위한 localStorage 입문 (0) | 2026.03.11 |
