CUBE.ai

바이브코딩

AI가 짠 코드, 머지 전에 확인할 5줄 — 주니어를 위한 리뷰 체크리스트

2026.08.25

목차

  1. 돌아간다는 것은 무엇을 증명해 줄까?
  2. 이 코드, 진짜 실행되는 코드일까?
  3. 실패했을 때는 어떻게 되나?
  4. 이미 있는 걸 또 만들지 않았나?
  5. 밖에서 들어온 값을 그대로 믿고 있지 않나?
  6. 이 코드를 내가 한 줄씩 설명할 수 있나?
  7. 이 5줄을 어떻게 습관으로 만들까?

전체 지도

머지 전에 확인할 사항

지난 글에서 우리는 규칙 파일로 생성 단계의 고삐를 잡았습니다. AI에게 "우리 집 규칙은 이렇다"라고 미리 알려주는 작업이었죠. 그런데 규칙을 아무리 잘 써도, 규칙은 요청일 뿐 보증이 아닙니다. 그래서 오늘은 반대쪽 끝, 통과 단계를 잡습니다.

지도를 보시면 가운데에 머지 버튼이 있고, 그 앞을 다섯 개의 관문이 막고 있습니다. 왼쪽 위부터 시계 방향으로 ① 실행 가능성 → ② 실패 경로 → ③ 중복 → ④ 외부 입력 → ⑤ 설명 가능성. 이 다섯 개가 오늘 글의 전부입니다. 그리고 지도 오른쪽 아래, 관문을 다 통과한 코드가 지나가는 좁은 문이 하나 더 그려져 있는데, 그게 마지막 섹션에서 만들 PR 템플릿입니다.


1. 돌아간다는 것은 무엇을 증명해 줄까?

머지(merge, 내 브랜치의 코드를 팀의 본 코드에 합치는 것) 버튼은 우리 집 현관문을 열어주는 일과 같습니다. 한 번 들어오면 그때부터는 손님이 아니라 식구예요. 그런데 많은 주니어분들이 문을 여는 기준을 딱 하나로 삼습니다. "화면에 잘 뜨네? 통과."

여기서 짚고 갈 게 있습니다. 돌아간다는 건 시동이 걸린다는 뜻이지, 브레이크가 있다는 뜻이 아닙니다. 실제 사례도 있어요. 2025년 유출 사고로 크게 알려진 Tea 앱은 화면상으로는 멀쩡히 동작하는 서비스였지만, 데이터베이스 접근 규칙을 손으로 설정하지 않은 탓에 인증 없이 사용자 사진과 대화가 통째로 열려 있었습니다. 데모에서는 절대 안 터지는 종류의 문제죠. 로그인한 내 계정으로만 눌러봤으니까요.

A_wide_horizontal_Korean_educational_sketch_note_-1787469187275.webp

숫자 하나만 던지고 넘어가겠습니다. CodeRabbit이 오픈소스 풀 리퀘스트(PR, 코드 합쳐달라는 요청) 470건을 뜯어봤더니, AI가 개입한 PR은 평균 10.83개, 사람이 직접 짠 PR은 평균 6.45개의 이슈를 갖고 있었습니다. 특히 XSS(악성 스크립트를 화면에 심는 공격) 계열은 AI 쪽이 2.74배 많았고요.

그렇다고 "AI 코드는 위험하니 쓰지 마세요"로 끝낼 생각은 없습니다. 오늘 글의 약속은 이겁니다. 길게 볼 필요 없습니다. 딱 5줄만 봅니다. 그리고 5줄 전부를 회원 목록 컴포넌트 하나(UserList.tsx)에서 뽑아내겠습니다. 예제가 하나면 머릿속에 남는 것도 하나거든요.

여기서 실제로 할 일 아직 아무것도 고치지 마세요. 최근에 AI 도움으로 만든 파일 하나만 열어두고, 아래 다섯 개 질문을 순서대로 던져보세요.

참고 자료


2. 이 코드, 진짜 실행되는 코드일까?

자, 첫 번째 관문입니다. 지도에서 보면 왼쪽 위 첫 번째 문이에요.

AI는 존재하지 않는 패키지, 존재하지 않는 함수, 존재하지 않는 옵션을 아주 그럴듯한 이름으로 지어냅니다. 이걸 환각(hallucination)이라고 부르죠. 메뉴판에는 근사하게 적혀 있는데 주방에는 그 재료가 아예 없는 식당을 떠올리시면 됩니다. 주문을 받는 태도는 완벽해요. 음식만 안 나올 뿐이죠.

Socket이 2026년 4월에 프런티어 모델 5종으로 약 20만 건의 응답을 받아 확인해 보니, 없는 패키지를 지어내는 비율이 모델별로 4.62%~6.10%였습니다. 게다가 지어낸 이름 중 53개는 다섯 모델이 공통으로 뱉어냈어요. 이름이 반복되면 공격자가 그 이름으로 진짜 악성 패키지를 미리 올려둘 수 있습니다. 이걸 슬롭스쿼팅(slopsquatting)이라고 부릅니다.

A_wide_horizontal_Korean_educational_sketch_note_-1787468903849.webp

// 나쁜 예 — 그럴듯하지만 존재하지 않는 패키지와 옵션
import { useFetch } from "react-use-fetch-ts";

export function UserList() {
  const { data } = useFetch<User[]>("/api/users", { autoRetry: 3 });
  return <ul>{data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
// 좋은 예 — 이미 프로젝트가 쓰고 있는 것으로
import { useQuery } from "@tanstack/react-query"; // package.json에 실제로 있는지 확인함

export function UserList() {
  const { data } = useQuery<User[]>({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then((r) => r.json()),
  });
  return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

30초 확인법 import 줄만 세로로 훑으세요. 처음 보는 이름이 있으면 package.json에서 검색하고, 없으면 설치하지 말고 지웁니다. 함수 이름은 공식 문서에서 그 이름 그대로 검색해 보세요. 검색 결과가 블로그뿐이고 공식 문서에 없다면, 그건 없는 함수입니다.

참고 자료


3. 실패했을 때는 어떻게 되나?

import를 다 확인했다고요? 좋습니다. 그런데 방금 좋은 예로 적은 코드에도 사실 구멍이 하나 남아 있습니다. 눈치채셨나요?

AI는 성공 경로만 상상하는 습관이 있습니다. "회원 목록을 보여줘"라고 하면 회원이 잘 내려온 상황만 그려요. 맑은 날만 상정하고 짠 소풍 계획서 같은 겁니다. 돗자리, 도시락, 프리스비까지 챙겼는데 우산 항목이 없죠.

A_wide_horizontal_Korean_educational_sketch_note_-1787468910001.webp

주니어가 볼 항목은 딱 네 가지입니다. 로딩 중 / 에러 / 빈 배열 / null. 이 네 개가 화면에서 각각 어떻게 보이는지 말할 수 있으면 통과입니다.

// 나쁜 예 — 맑은 날만 있는 코드
export function UserList() {
  const { data } = useQuery<User[]>({ queryKey: ["users"], queryFn: fetchUsers });
  return <ul>{data!.map((u) => <li key={u.id}>{u.profile.nickname}</li>)}</ul>;
}

data!의 느낌표는 "없을 리 없다"고 타입스크립트에게 우기는 표시입니다. 그런데 응답이 오기 전에는 반드시 없어요. u.profile.nickname도 마찬가지입니다. 프로필을 아직 안 만든 회원 한 명이면 화면 전체가 하얗게 꺼집니다.

// 좋은 예 — 네 가지 경우가 전부 화면을 갖는다
export function UserList() {
  const { data, isLoading, isError, refetch } = useQuery<User[]>({
    queryKey: ["users"],
    queryFn: fetchUsers,
  });

  if (isLoading) return <Spinner />;
  if (isError) return <ErrorBox message="회원 목록을 불러오지 못했어요." onRetry={refetch} />;
  if (!data || data.length === 0) return <EmptyState message="아직 등록된 회원이 없습니다." />;

  return (
    <ul>
      {data.map((u) => (
        <li key={u.id}>{u.profile?.nickname ?? u.email}</li>
      ))}
    </ul>
  );
}

30초 확인법 개발자 도구 네트워크 탭에서 오프라인으로 바꾸고 화면을 새로고침하세요. 하얀 화면이 나오면 실패입니다. 사람이 읽을 수 있는 문장이 하나라도 떠야 통과입니다.

참고 자료


4. 이미 있는 걸 또 만들지 않았나?

이건 터지지 않아서 더 무서운 항목입니다. 앞의 두 개는 언젠가 에러로 존재를 알리지만, 이건 조용히 쌓이거든요.

AI는 지금 열어둔 파일만 봅니다. 우리 레포 어딘가에 formatDate가 이미 있다는 걸 모르니, 필요하면 그냥 새로 하나 만들어 줍니다. 서랍에 가위가 있는데 매번 새로 사 오는 셈이죠. 한두 번은 티가 안 나요. 서랍이 가위로 가득 찰 때쯤 문제가 됩니다. Remove_the_whiteboard_polaroid_rectangular_fra-1787469008363.webp

GitClear가 2023년부터 2026년까지 코드 변경 6억 2,300만 줄을 추적했더니, 5줄 이상 반복되는 중복 블록이 100만 줄당 40.3개에서 73.0개로 81% 늘었습니다. 반대로 기존 코드를 옮겨 정리하는 리팩터링(구조를 다시 손보는 작업)의 비중은 21%에서 3.8%로 떨어졌고요. 새로 짜기는 쉬워지고, 정리하기는 안 하게 된 겁니다.

// 나쁜 예 — UserList.tsx 안에서 태어난 세 번째 formatDate
function formatDate(d: string) {
  const date = new Date(d);
  return `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}`;
}
// 좋은 예 — 이미 있는 걸 가져다 쓴다
import { formatDate } from "@/utils/date"; // src/utils/date.ts에 이미 존재

30초 확인법 함수 이름으로 검색하면 안 나옵니다. AI가 지은 이름과 우리 레포의 이름이 다르니까요. 기능 키워드로 찾으세요. formatDate가 아니라 getFullYear, toLocale, padStart 같은 속 재료로 레포 전체를 검색하면 형제들이 우르르 나옵니다.

참고 자료


5. 밖에서 들어온 값을 그대로 믿고 있지 않나?

지도의 네 번째 문입니다. 여기가 유일하게 "나중에 고치죠"가 안 통하는 자리예요.

밖에서 들어온 값이란 세 가지입니다. 사용자가 입력한 것, 주소창에 붙어 온 것, 서버가 응답으로 준 것. 이 셋을 검증 없이 화면이나 쿼리에 그대로 꽂는 순간, 우리 서비스 출입구는 신분 확인 없이 통과시키는 문이 됩니다. 앞에서 본 XSS 2.74배가 바로 여기서 나옵니다.

A_wide_horizontal_Korean_educational_sketch_note_-1787468992064.webp

여기에 하나 더 붙일 게 API 키 하드코딩(비밀 값을 코드 안에 그대로 박아두는 것)입니다. RedHunt Labs가 노코드·AI 개발 플랫폼으로 만들어진 사이트 13만 개를 훑어보니, 약 20%에서 최소 하나 이상의 비밀 값이 그대로 노출돼 있었습니다. 프런트엔드 코드는 누구나 열어볼 수 있다는 걸, AI는 굳이 상기시켜 주지 않습니다.

// 나쁜 예 — 세 가지 문제가 한 화면에
const res = await fetch(`/api/users?role=${searchParams.get("role")}`, {
  headers: { Authorization: "Bearer sk_live_9f2a7c1b4e8d" }, // 키가 코드 안에
});

<div dangerouslySetInnerHTML={{ __html: user.bio }} />  // 소개글에 스크립트가 들어오면?
// 좋은 예 — 값은 좁히고, 키는 서버 뒤로, 화면은 텍스트로
const ROLES = ["admin", "member", "guest"] as const;
type Role = (typeof ROLES)[number];

const raw = searchParams.get("role") ?? "";
const role: Role = ROLES.includes(raw as Role) ? (raw as Role) : "member"; // 허용 목록으로 좁힌다

const res = await fetch(`/api/users?role=${role}`); // 키는 서버 라우트가 대신 붙인다

<p className="user-bio">{user.bio}</p>  // 텍스트로 넣으면 스크립트는 그냥 글자다

30초 확인법 파일에서 딱 세 개만 검색하세요. innerHTML, 문자열로 조립된 쿼리(백틱 안의 ${), 그리고 따옴표 안에 든 20자 넘는 영문·숫자 뒤범벅 문자열. 세 개 다 없으면 통과입니다.

참고 자료


6. 이 코드를 내가 한 줄씩 설명할 수 있나?

여기까지 오셨으면 코드는 돌아가고, 실패해도 안 죽고, 중복도 없고, 입력도 안전합니다. 그런데 마지막 문이 하나 남았어요. 그리고 이 문은 도구가 대신 열어줄 수 없습니다.

A_wide_horizontal_Korean_educational_sketch_note_-1787468995012.webp

Stack Overflow 개발자 설문에서 응답자의 66%가 "거의 맞지만 정확하지는 않은" AI 답변을 문제로 꼽았고, 45%는 AI가 짠 코드를 디버깅하는 게 사람이 짠 코드보다 시간이 더 걸린다고 답했습니다. 이유는 단순해요. 내가 안 쓴 코드는 내 머릿속에 지도가 없거든요. 남의 노트 필기를 그대로 들고 시험장에 들어간 상황과 같습니다. 필기는 완벽한데, 응용문제가 나오면 손이 안 움직입니다.

// 나쁜 예 — 왜 있는지 모르는 채로 남겨둔 줄
const sorted = useMemo(() => [...users].sort((a, b) => b.score - a.score), []);

의존성 배열이 []라서 users가 바뀌어도 정렬은 다시 안 됩니다. 그런데 "AI가 넣었으니 이유가 있겠지" 하고 넘어가면, 두 달 뒤 "목록이 갱신이 안 돼요" 버그의 범인이 됩니다.

// 좋은 예 — 이유를 아는 만큼만 남긴다
// users가 바뀔 때만 다시 정렬. 목록이 500개 넘어가서 매 렌더 정렬을 피하려고 useMemo 사용.
const sorted = useMemo(() => [...users].sort((a, b) => b.score - a.score), [users]);

30초 확인법 AI에게 "왜 이렇게 짰는지" 물어보고, 그 답을 내 말로 다시 한 줄 적어보세요. 적히면 통과, 안 적히면 머지 금지입니다. 이건 예외 없는 조건으로 못 박으시길 권합니다.

참고 자료


7. 이 5줄을 어떻게 습관으로 만들까?

체크리스트의 최대 적은 "까먹는 것"입니다. 그래서 기억에 맡기지 말고, 매번 눈앞에 뜨게 만들어 두는 게 훨씬 낫습니다. 아래 블록을 그대로 복사해서 레포의 .github/PULL_REQUEST_TEMPLATE.md에 넣어두세요. 그러면 PR을 열 때마다 다섯 줄이 자동으로 따라 올라옵니다.

A_wide_horizontal_Korean_educational_sketch_note_-1787469002153.webp

## AI 코드 5줄 체크

- [ ] ① 실행되는 코드인가 — 모든 import와 API 이름을 공식 문서에서 확인했다
- [ ] ② 실패 경로가 있는가 — 로딩 / 에러 / 빈 배열 / null 네 가지 화면을 확인했다
- [ ] ③ 중복이 아닌가 — 기능 키워드로 레포를 검색해 기존 구현이 없음을 확인했다
- [ ] ④ 외부 입력을 믿지 않았나 — innerHTML, 문자열 조립 쿼리, 하드코딩된 키가 없다
- [ ] ⑤ 한 줄씩 설명할 수 있나 — 설명 못 하는 줄은 남기지 않았다

> ⑤에 체크가 안 되면 머지하지 않습니다.

마지막으로 순서 이야기를 하나만 하고 마치겠습니다. ①부터 ④까지는 사실 도구가 점점 대신해 주고 있는 영역입니다. 린터가 없는 import를 잡아주고, 타입 검사기가 null을 잡아주고, 보안 스캐너가 하드코딩된 키를 잡아줍니다. 몇 년 뒤에는 지금보다 훨씬 많이 자동화되어 있을 거예요.

그런데 ⑤는 다릅니다. "이 코드를 내가 설명할 수 있는가"는 도구가 대신 체크해 줄 수 없습니다. 설명할 수 있다는 건 코드에 대한 판단을 내 이름으로 책임진다는 뜻이고, 그 책임은 아직 사람에게만 있거든요. 그래서 ⑤를 맨 끝에 뒀습니다. 앞의 넷을 통과시키는 건 실력이지만, ⑤를 통과시키는 건 태도입니다.

자, 그런데 여기서 자연스럽게 다음 질문이 따라오죠. "설명할 수 있어야 한다는 건 알겠는데, 그래서 내가 안 짠 코드를 어떻게 디버깅하냐는 거예요." 맞습니다. 그게 다음 글의 주제입니다.

여기서 실제로 할 일 지금 열려 있는 레포 하나에 .github/PULL_REQUEST_TEMPLATE.md를 만들고 위 블록을 붙여넣으세요. 5분이면 끝납니다.

참고 자료


댓글

불러오는 중입니다…