버그 없는 코드의 원칙
DRY, Single Source of Truth, State Management, 관심사의 분리
이 챕터가 이 책의 핵심이다. AI가 코드를 짜주더라도, 이 원칙을 모르면 앱이 커질수록 무너진다.
DRY — Don't Repeat Yourself
“반복하지 마라”는 소프트웨어의 가장 기본적인 원칙이다. 같은 코드가 두 곳 이상에 있으면, 하나를 고칠 때 나머지를 깜빡하게 된다. 세 곳이면 하나는 반드시 놓친다. 열 곳이면 시스템이 무너진다.
❌ DRY 위반 — 바이브 코딩에서 가장 흔한 버그
// 페이지 A에서 사용자 이름 표시 const displayName = user.firstName + " " + user.lastName; // 페이지 B에서도 같은 로직 const name = user.firstName + " " + user.lastName; // 페이지 C에서는 다르게... (버그!) const fullName = user.lastName + user.firstName;
세 곳에서 같은 일을 다르게 하고 있다. 이름 형식을 바꾸려면 세 곳을 모두 찾아서 고쳐야 한다.
✅ DRY 준수 — 한 곳에서 관리
// utils/format.ts — 한 곳에서 정의
function getDisplayName(user) {
return user.firstName + " " + user.lastName;
}
// 모든 페이지에서 이 함수만 쓴다
const name = getDisplayName(user);이름 형식을 바꾸려면 함수 하나만 수정하면 된다. 모든 페이지에 자동 반영된다.
Single Source of Truth (SSOT)
“진실의 원천은 하나여야 한다.” 같은 데이터가 여러 곳에 저장되면, 어느 것이 “진짜”인지 알 수 없게 된다.
🏥 병원 비유
환자의 알레르기 정보가 진료실 차트, 약국 시스템, 간호사 메모 세 곳에 따로 적혀 있다. 진료실에서 알레르기를 업데이트했는데 약국 시스템은 안 바뀌었다면? — 잘못된 약이 처방될 수 있다.
SSOT 적용: 알레르기 정보는 환자 DB 한 곳에만 저장하고, 진료실·약국·간호사 모두 그 한 곳을 참조한다.
바이브 코딩에서 SSOT 위반은 AI가 가장 자주 만드는 실수 중 하나다. AI에게 “이 페이지에 사용자 정보를 표시해줘”라고 하면, AI는 그 페이지에 사용자 정보를 따로 가져오는 코드를 만든다. 여러 페이지에 같은 요청을 하면, 각 페이지가 독립적으로 사용자 정보를 관리하게 된다. 한 페이지에서 정보를 수정해도 다른 페이지에는 반영되지 않는다.
💡 AI에게 SSOT를 지시하는 법
“사용자 데이터는 반드시 하나의 store(또는 context, 또는 hook)에서 관리하고, 모든 컴포넌트는 그 store에서 가져다 써. 절대 컴포넌트 안에서 따로 사용자 데이터를 fetch하지 마.”
State Management — 중앙화된 상태 관리
상태(state)란 앱에서 “변하는 데이터”다. 로그인 여부, 장바구니 내용, 다크모드 설정, 현재 열린 탭 — 모두 상태다. 상태 관리가 바이브 코딩에서 가장 많은 버그를 만드는 영역이다.
🎵 오케스트라 비유
오케스트라에서 각 파트(바이올린, 첼로, 플루트)가 각자 악보를 해석하면 불협화음이 난다. 지휘자가 하나의 악보를 기준으로 전체를 조율해야 한다.
앱도 마찬가지다. 각 컴포넌트(페이지, 버튼, 모달)가 각자 상태를 관리하면 데이터가 불일치한다. 중앙 store 하나가 전체 상태를 관리해야 한다.
로컬 상태 vs 글로벌 상태
로컬 상태
한 컴포넌트 안에서만 쓰이는 상태. 모달 열림/닫힘, 입력 필드 값, 드롭다운 선택. useState로 관리.
글로벌 상태
여러 컴포넌트가 공유하는 상태. 로그인 유저 정보, 장바구니, 테마 설정. Context, Zustand, Redux 등으로 관리.
❌ 상태 관리 실패 — 실제 바이브 코딩 사례
mathon 앱 개발 중 겪은 문제: 사용자 프로필을 settings 페이지에서 수정하면, 헤더의 이름은 그대로, 대시보드의 이름은 바뀌고, 사이드바의 이름은 새로고침해야 바뀌는 상황. 세 곳이 각각 다른 방식으로 사용자 데이터를 가져오고 있었기 때문이다.
해결: 중앙 Store 패턴
✅ 중앙 Store 적용
// store/user-store.ts — 진실의 원천
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
updateName: (name) => set((s) => ({
user: { ...s.user, name }
})),
}));
// 헤더, 대시보드, 사이드바 모두:
const { user } = useUserStore();
// → 한 곳에서 바꾸면 전체 자동 반영💡 AI에게 상태 관리를 지시하는 법
프로젝트 시작 시: “글로벌 상태 관리는 Zustand를 사용해. 사용자 정보, 인증 상태, 앱 설정은 각각 별도의 store로 만들고, 컴포넌트에서는 반드시 store를 통해서만 데이터에 접근해. 컴포넌트 안에서 직접 fetch하지 마.”
관심사의 분리 (Separation of Concerns)
하나의 파일이 너무 많은 일을 하면 문제가 생긴다. 화면을 그리면서, 데이터를 가져오고, 비즈니스 로직도 처리하고, 에러도 잡는 파일 — 500줄짜리 괴물이 된다. 한 곳을 고치면 다른 곳이 깨진다.
🏠 집의 배선 비유
전기선, 수도관, 가스관이 모두 한 파이프에 들어있으면? 수도관을 고치려다 전기선을 건드려서 정전이 난다. 각각 독립된 배선으로 분리해야 안전하다. 코드도 마찬가지다.
세 가지 레이어
components/hooks/ 또는 utils/services/ 또는 api/❌ 관심사 혼합 — AI가 자주 만드는 코드
// 한 컴포넌트에 모든 것이 섞여있다
function Dashboard() {
// 데이터 레이어 (DB 접근)
const [data, setData] = useState([]);
useEffect(() => {
fetch("/api/sessions").then(r => r.json())
.then(setData);
}, []);
// 로직 레이어 (비즈니스 규칙)
const completedSessions = data.filter(
s => s.status === "completed"
);
const avgDuration = completedSessions.reduce(
(sum, s) => sum + s.duration, 0
) / completedSessions.length;
// UI 레이어 (화면)
return <div>평균: {avgDuration}분</div>;
}✅ 관심사 분리 적용
// hooks/useSessionStats.ts (로직 + 데이터)
function useSessionStats() {
const { data } = useSessions(); // 데이터
const completed = data.filter( // 로직
s => s.status === "completed"
);
const avg = completed.reduce(
(sum, s) => sum + s.duration, 0
) / completed.length;
return { completed, avg };
}
// components/Dashboard.tsx (UI만)
function Dashboard() {
const { avg } = useSessionStats();
return <div>평균: {avg}분</div>;
}💡 AI에게 분리를 지시하는 법
“컴포넌트 파일에는 UI만 넣어. 데이터 fetching은 hooks/ 폴더에 커스텀 훅으로 분리하고, API 호출은 services/ 폴더에 분리해. 한 파일이 200줄을 넘으면 반드시 분리해.”