2장. 실행과 검증 루프

학습 목표

  • 로컬 실행, 개발 서버, 포트의 관계를 설명하고 처음 받은 저장소를 실행할 수 있게 준비한다.
  • 테스트·타입 검사·린트·빌드가 각각 어떤 문제를 잡고 무엇을 놓치는지 구분한다.
  • 실패 출력에서 무엇이, 어디서, 왜 틀렸는지 읽고 가장 먼저 볼 오류를 고른다.
  • 가설을 하나씩 확인하며 실패 원인을 좁힌다.
  • 브라우저 확인과 증거 요구로 '완료' 주장을 검증하고, 검증하지 못한 것은 그렇다고 밝힌다.

들어가며

"수정 완료했습니다. 모든 테스트가 통과합니다." 코딩 에이전트와 일하다 보면 이 문장을 하루에도 여러 번 봅니다. 그런데 테스트가 정말 실행됐는지, 실행된 테스트가 이번 변경을 다루는지, 화면에서도 문제가 사라졌는지는 문장만으로 알 수 없습니다.

한 사이드 프로젝트 운영자는 에이전트의 보고를 믿고 배포했다가 다음 날 아침 문의를 받았습니다. 테스트는 정말 모두 통과했습니다. 다만 테스트는 계산 함수만 다뤘고, 고친 버그는 화면에 있었습니다. 브라우저를 한 번만 열어 봤어도 알 수 있었던 일입니다.

이 장은 "됐다"는 말을 "됐다는 증거"로 바꾸는 기술을 다룹니다. 앱이 어디서 돌아가는지부터 시작해 로컬 실행 준비, 검사 도구별 역할, 오류 메시지 읽는 법, 원인을 좁히는 절차를 차례로 봅니다. 이어 검증 루프를 사람이 기억하지 않아도 돌아가게 만드는 장치, 브라우저 확인, 증거 요구, 맥락 정리를 다룹니다.

핵심 개념

앱이 도는 자리: 브라우저, 서버, 저장소

웹앱은 보통 세 곳에서 돌아갑니다. 사용자의 브라우저는 화면을 그리고 버튼 클릭을 받습니다. 서버는 브라우저의 요청을 받아 계산하고 응답을 돌려줍니다. 저장소(데이터베이스나 파일)는 새로고침해도 남아야 하는 데이터를 보관합니다. 식당에 비유하면 브라우저는 홀, 서버는 주방, 데이터베이스는 창고입니다.

이 구분이 중요한 이유는 오류가 나타나는 곳이 서로 다르기 때문입니다.

문제가 난 곳 오류가 보이는 곳
브라우저(화면 코드) 브라우저 개발자 도구의 콘솔
브라우저와 서버 사이(요청·응답) 개발자 도구의 네트워크 탭, 서버 출력
서버 코드 개발 서버를 띄운 터미널, 배포 후에는 서버 로그
데이터베이스 연결 서버 출력의 연결 오류(예: 연결 거부를 뜻하는 ECONNREFUSED. 데이터베이스가 꺼져 있거나 주소·포트가 틀렸을 때)

"화면에 저장됐다고 떴다"와 "데이터가 저장됐다"는 다른 사실입니다. 저장은 새로고침하거나 다른 기기에서 다시 열어 봐야 확인됩니다.

로컬 실행: 개발 서버와 포트

로컬 실행은 앱을 인터넷에 올리기 전에 내 컴퓨터에서 돌려 보는 것입니다. 정식 개업 전의 가족 시식회와 같습니다. 앱을 띄워 주는 프로그램을 개발 서버라 하고, 개발 서버는 번호가 붙은 문인 포트(port)를 열고 요청을 기다립니다. http://localhost:3000은 '내 컴퓨터의 3000번 문'이라는 뜻입니다.

개발 서버를 켜다 EADDRINUSE(address already in use) 오류를 만나면, 그 포트를 이미 다른 프로세스가 쓰고 있다는 뜻입니다. 흔히 전에 띄운 개발 서버가 다른 터미널에 아직 살아 있습니다. 대응은 그 프로세스를 끄거나 다른 포트로 띄우는 것입니다. 끌 때는 포트를 쓰는 프로세스의 번호(PID)를 확인하고(macOS와 Linux에서는 lsof -i :3000) kill <PID>로 그 프로세스만 종료합니다. 이름 패턴으로 프로세스를 한꺼번에 끄는 명령은 관계없는 프로그램까지 끌 수 있어 위험합니다. 이 오류는 대개 포트 점유 문제라 재설치로는 풀리지 않습니다. 드물게 같은 서버를 두 번 띄우는 코드나 설정이 원인일 때도 있습니다.

처음 받은 저장소를 실행할 수 있게 준비하기

남이 만든 저장소, 또는 에이전트가 만든 저장소를 처음 받으면 바로 npm run dev부터 치고 싶어집니다. 하지만 실행은 몇 가지 준비 위에서만 됩니다. 순서대로 확인하면 헤매는 시간이 줄어듭니다.

  1. README와 지침 파일을 읽는다. 실행 명령과 필요한 준비가 적혀 있는 곳입니다.
  2. 런타임 버전을 맞춘다. 저장소에 .nvmrc 같은 버전 파일이 있으면 그 버전의 Node.js를 씁니다.
  3. lock 파일대로 설치한다. package-lock.json이 있으면 npm ci로 기록된 버전을 그대로 설치합니다. npm 문서는 이 명령이 lock 파일 없이는 돌지 않고, package.json과 내용이 다르면 맞춰 고치는 대신 오류를 내며 멈추고, package.json도 lock 파일도 건드리지 않는다고 설명합니다.
  4. 환경 변수를 채운다. .env.example을 보고 필요한 이름을 확인해 .env.local 같은 로컬 파일에 테스트용 값을 넣습니다.
  5. 데이터베이스를 준비한다. 마이그레이션 명령이 있으면 실행해 표를 만듭니다.
  6. 개발 서버를 켜고 핵심 화면 하나를 직접 열어 본다.

이 단계를 건너뛰면 나타나는 오류는 대개 정해져 있습니다. 환경 변수가 없다는 오류는 4단계, Cannot find module은 3단계, 데이터베이스 연결 거부는 5단계를 다시 보라는 신호입니다.

검사 도구 지도

코드를 확인하는 자동 검사는 여러 종류이고, 각자 잡는 것이 다릅니다.

도구 비유 잡는 문제 못 잡는 문제
테스트 시식단의 맛 평가 정해 둔 입력에 기대한 결과가 나오는지 테스트를 쓰지 않은 동작
타입 검사 설계도 치수 대조 함수에 넘기는 값의 종류와 개수, 없는 속성 사용 값의 형식은 맞는데 계산 논리가 틀린 경우
린트 주방 위생 점검표 쓰지 않는 변수, 위험한 코드 패턴, 규칙 위반 기능이 요구대로 동작하는지
빌드 개업 전 최종 조립 배포용으로 묶을 때만 드러나는 오류 실행 중에 생기는 논리 오류

TypeScript에서는 tsc --noEmit처럼 실행 파일을 만들지 않고 타입 검사만 하는 명령을 씁니다. TypeScript 문서는 noEmit을 컴파일 결과물을 내보내지 않고 TypeScript를 타입 검사기로 쓰는 옵션이라고 설명합니다. 함수에 필수 인자를 하나 늘렸다면 타입 검사 한 번으로 맞지 않는 호출부를 찾을 수 있습니다. 다만 any처럼 타입 검사를 피해 간 호출은 놓칠 수 있습니다.

핵심은 '통과'의 뜻이 도구마다 다르다는 점입니다. 테스트가 통과했다고 테스트가 다루지 않는 화면까지 맞는 것은 아니고, 타입 검사가 통과했다고 할인율 계산이 맞는 것도 아닙니다. 그래서 변경의 성격에 맞는 검사를 고르고, 가벼운 검사부터 돌립니다.

오류 메시지 해부

오류 메시지는 겁을 주려고 길게 나오는 것이 아닙니다. 대개 세 가지를 알려 줍니다.

  1. 무엇이: 오류의 종류(TypeError, 실패한 테스트 이름)
  2. 어디서: 파일 이름과 줄 번호
  3. 왜: 기대한 값과 실제 값, 또는 무엇이 없었는지
TypeError: Cannot read properties of undefined (reading 'filter')
    at ClassSchedule (src/components/ClassSchedule.tsx:31:18)
    at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:15486:18)

첫 줄은 "아직 값이 없는(undefined) 대상에 filter를 쓰려 했다"는 뜻입니다. 그 아래 스택 트레이스(stack trace)는 오류가 날 때까지 거친 함수들을 안쪽부터 나열한 것입니다. node_modules 안의 줄은 남이 만든 라이브러리이고, src/ 안의 줄이 내 코드입니다. 대부분은 내 코드 중 가장 위에 있는 줄, 여기서는 ClassSchedule.tsx 31번째 줄부터 엽니다. 데이터를 불러오기 전에 목록을 그리려 했을 가능성이 큽니다.

오류가 수십 개 쏟아질 때는 맨 처음 오류부터 봅니다. 모듈 하나를 찾지 못하면 그 모듈을 쓰는 모든 곳에서 연쇄 오류가 나기 때문입니다. 첫 원인을 고치면 뒤의 오류 대부분이 함께 사라집니다.

명령의 성공과 실패를 가장 확실하게 알려 주는 것은 문구가 아니라 종료 코드(exit code)입니다. 종료 코드 0은 성공, 0이 아닌 값은 실패를 뜻합니다. 경고가 잔뜩 찍혀도 종료 코드가 0이면 명령은 성공한 것이고, 마지막 줄이 조용해도 0이 아니면 실패입니다. 직전 명령의 종료 코드는 터미널에서 echo $?로 봅니다. 또 프로그램은 보통 결과를 표준 출력(stdout)에, 오류를 표준 오류(stderr)에 따로 씁니다. 출력이 길어 에이전트에게 통째로 넘길 때는 npm test > test.log 2>&1처럼 표준 출력을 파일로 보내고(>) 표준 오류도 같은 곳으로 합치면(2>&1) 빠짐이 없습니다.

실패 출력으로 원인 좁히기

원인을 찾는 일은 탐정 일과 같습니다. 단서를 모으고, 용의자를 하나씩 확인하고, 확인되지 않은 추측으로 사람을 체포하지 않습니다.

  1. 재현한다. 같은 절차로 같은 실패가 나오는지 확인합니다. 재현되지 않으면 고쳤는지도 알 수 없습니다.
  2. 실패가 난 단계를 가린다. 설치, 시작, 빌드, 실행 중 어디서 실패했는지에 따라 볼 곳이 다릅니다.
  3. 가장 처음의 관련 오류를 고른다. 연쇄 오류는 일단 미룹니다.
  4. 가설 하나를 세운다. "이 import 경로가 바뀌었다"처럼 확인 가능한 문장으로 씁니다.
  5. 그 가설만 확인한다. 여러 곳을 한꺼번에 고치면 무엇이 효과를 냈는지 알 수 없습니다.
  6. 수정 뒤 같은 검사를 다시 돌린다. 처음 실패했던 바로 그 명령으로 확인합니다.

에이전트에게 실패를 넘길 때는 출력 전체를 붙이고 "원인을 고치고, 오류를 덮지 마라"고 말합니다. Claude Code 모범 사례도 빌드 실패를 맡길 때 오류 원문을 붙이고 성공을 확인하게 하되, 오류를 억누르지 말고 근본 원인을 고치라고 지시하길 권합니다. 오류를 덮는 대표적인 방법이 타입 검사 끄기 주석, 테스트 건너뛰기, 기대값을 실제값에 맞춰 고치기입니다. 기대값이 요구 사항과 같다면 틀린 쪽은 코드입니다.

검증 루프와 강제 장치

검증 루프는 고치기, 검사 실행, 결과 읽기를 검사가 통과할 때까지 반복하는 흐름입니다. 에이전트에게 검사 명령을 알려 주면 이 루프를 스스로 돌리지만, 사람이 매번 말해야 하고 에이전트가 가끔 잊습니다. 그래서 루프의 일부를 장치로 고정합니다.

장치 하는 일 예
작업 지시 이번 작업에서 돌릴 검사를 알려 준다 "구현 뒤 npm test를 돌리고 출력을 보여 줘"
지침 파일 모든 작업에 공통인 검사 명령을 알려 준다 "작업이 끝나면 npm run check"
훅 정해진 시점에 검사를 예외 없이 실행한다 파일을 고칠 때마다 포매터 실행, 끝내기 전에 검사 스크립트
CI 필수 검사 검사를 통과하지 못한 변경은 병합되지 않게 한다 풀 리퀘스트마다 테스트, 실패하면 병합 버튼이 막힘

Claude Code 문서는 훅이 지침 파일과 달리 결정적으로 동작해 그 행동이 반드시 일어나게 보장한다고 설명합니다. 예를 들어 PostToolUse 이벤트에 Edit|Write 조건을 붙인 훅은 에이전트가 그 편집 도구로 파일을 고친 직후마다 포매터를 실행합니다. 셸 명령으로 바뀐 파일은 잡지 못하므로, 빠짐없이 보려면 끝낼 때 한 번 더 검사합니다. 이 훅을 프로젝트의 .claude/settings.json에 적어 커밋하면 팀이 같은 훅을 공유합니다. 대화형 세션에서는 그 폴더를 신뢰한다고 승인해야 훅이 돌고, 설정으로 훅을 끌 수도 있습니다. Stop 훅은 에이전트가 응답을 끝내려 할 때 검사 스크립트를 돌려, 실패하면 작업을 이어 가게 만듭니다. 다만 문서에 따르면 도구 호출 없이 여덟 번 연속 붙잡으면 그다음에는 끝내게 두므로 통과를 무조건 보장하지는 않습니다.

GitHub의 보호 브랜치(protected branch)에서 '병합 전 상태 검사 통과 필수'를 켜면, 지정한 필수 검사가 성공(또는 건너뜀·중립)으로 끝나야 그 브랜치에 병합할 수 있습니다. 리뷰어가 깜빡해도 깨진 변경이 들어가지 않습니다. 기본 설정에서는 관리자가 이 규칙에서 빠지므로, 예외 없이 적용하려면 우회 금지 옵션도 켭니다. CI(지속적 통합, 변경마다 자동으로 검사를 돌리는 시스템)에서는 의존성을 npm ci로 설치합니다. npm 문서도 이 명령을 CI 같은 자동화 환경용이라고 설명합니다.

브라우저에서 사용자처럼 확인하기

화면이 바뀌는 작업은 브라우저에서 직접 확인해야 끝납니다. Anthropic의 장기 실행 에이전트 실험에서도, 에이전트는 기능을 제대로 시험하지 않은 채 완료로 표시하는 경향이 있었고, 브라우저 자동화로 사람 사용자처럼 시험하라고 명시하자 코드만 읽어서는 놓친 버그를 찾아냈습니다. Claude Code 모범 사례도 화면 변경은 결과 화면을 캡처해 시안과 비교하고 차이를 고치게 하라고 권합니다.

직접 확인할 때는 다음을 봅니다.

  • 버튼을 누르고 입력하는 흐름을 끝까지 해 본 뒤, 새로고침해도 결과가 남는지
  • 휴대전화 너비(예: 360px)로 줄였을 때 잘리거나 겹치는 곳이 없는지
  • 빈 데이터, 잘못된 입력, 느린 응답처럼 1장에서 정한 상태들
  • 개발자 도구의 콘솔: 빨간 자바스크립트 오류가 없는지
  • 개발자 도구의 네트워크 탭: 요청이 실제로 나갔는지, 응답의 상태 코드가 무엇인지

상태 코드는 응답의 결과를 숫자로 요약합니다. 200번대는 성공, 400번대는 요청 쪽 문제(잘못된 입력, 권한 없음, 없는 주소), 500번대는 서버 쪽 문제입니다. 버튼을 눌러도 반응이 없을 때 네트워크 탭에 요청이 아예 없으면 화면 코드를, 요청이 500으로 실패했으면 서버 출력을 봅니다.

주장과 증거를 구분하기

에이전트의 "완료했습니다"는 주장입니다. 증거는 실행한 명령과 그 원본 출력, 화면 캡처, 확인 절차와 결과입니다. Claude Code 모범 사례는 성공을 단언하게 하지 말고 테스트 출력, 실행한 명령과 그 결과, 결과 화면 같은 증거를 보이게 하라고 권합니다. 증거 검토가 검증을 다시 돌리는 것보다 빠르고, 지켜보지 않은 세션에도 통하기 때문입니다.

좋은 완료 보고에는 확인한 것과 함께 확인하지 못한 것이 들어 있습니다.

실행한 검사: npm test (42개 통과, 0개 실패), npx tsc --noEmit (오류 없음)
화면 확인: 휴대전화·데스크톱 너비 캡처 첨부. 새로고침 뒤 저장 유지 확인
확인하지 못한 것: 실제 문자 발송(테스트에서는 발송 기록만 확인)

같은 맥락에서 일한 에이전트는 같은 착오를 반복해 '확인'해 줄 수 있습니다. 그래서 Claude Code 문서는 일한 에이전트가 스스로 채점하지 않도록, 새 맥락의 하위 에이전트가 결과를 반박해 보게 하는 방법을 소개합니다.

맥락이 꼬였을 때는 새로 시작한다

1장에서 본 것처럼 대화의 컨텍스트에는 실패한 시도도 모두 쌓입니다. 같은 문제를 여러 번 고치다 보면 실패한 접근이 쌓인 맥락이 같은 실수를 부릅니다. Claude Code 모범 사례는 한 세션에서 같은 문제를 두 번 넘게 교정했는데도 틀린다면 /clear로 맥락을 비우고, 그동안 배운 점을 담은 더 구체적인 지시로 다시 시작하라고 권합니다. 깨끗한 세션과 더 나은 지시가 교정이 쌓인 긴 세션보다 거의 언제나 낫다는 것입니다.

새로 시작하기 전에 두 가지를 기억합니다. 첫째, 대화를 비워도 디스크의 코드는 그대로입니다. 실패한 시도의 변경이 남아 있다면 마지막으로 검증된 커밋으로 되돌릴지 먼저 정합니다(3장). 둘째, 새 세션에 넘길 인계 메모를 남깁니다. 무엇을 시도했고, 무엇이 실패했으며, 어떤 가설이 배제됐는지 적어 두면 같은 길을 다시 걷지 않습니다.

대화를 버리기 아까우면 /compact <지시>로 지시한 내용에 초점을 맞춰 요약합니다.

깊이 읽기

통과하는 테스트와 쓸모 있는 테스트

테스트가 통과한다는 사실은 그 테스트가 쓸모 있다는 증거가 아닙니다. 쓸모없는 테스트는 버그가 있어도 통과합니다. 대표적인 경우는 네 가지입니다.

쓸모없는 테스트 모습 확인하는 법
구현을 베낀 테스트 기대값을 코드와 같은 방식으로 계산한다 기대값이 요구 사항에서 독립적으로 나왔는지 본다
너무 약한 단언 "결과가 존재한다"만 확인한다 구체적인 값이나 상태를 비교하는지 본다
한 번도 실패한 적 없는 테스트 고치기 전에도 통과했다 고친 코드를 잠시 되돌려 실패하는지 본다
결과에 맞춰 고친 테스트 실패하자 기대값을 바꿨다 diff에서 테스트 파일의 기대값 변경을 찾는다

가장 강력한 확인은 먼저 실패를 보는 것입니다. 버그를 고칠 때 재현 테스트를 먼저 쓰고, 고치기 전에 그 테스트가 실패하는지 확인합니다. Claude Code 모범 사례도 증상을 설명한 뒤 "재현하는 실패 테스트를 쓰고, 그다음 고쳐라"라고 맡기는 예를 듭니다. 수정 전에 실패하고 수정 뒤에 통과한 테스트만이 그 수정을 증명합니다.

비슷한 함정이 스냅샷 테스트(화면이나 출력의 이전 모습을 저장해 두고 비교하는 테스트)의 일괄 갱신입니다. 실패한 스냅샷을 한꺼번에 '새 기준'으로 덮어쓰면, 의도한 변화와 의도하지 않은 변화가 함께 정답이 됩니다. 갱신하기 전에 실패한 스냅샷마다 이번 작업이 바꾸려던 것인지 확인합니다.

가끔 실패하는 테스트

코드를 바꾸지 않았는데 어떨 때는 통과하고 어떨 때는 실패하는 테스트를 불안정한 테스트(flaky test)라 합니다. 다시 돌려서 통과하면 넘어가고 싶지만, 대부분은 진짜 원인이 있습니다.

  1. 테스트끼리 상태를 공유한다. 같은 테스트용 데이터베이스나 전역 변수를 쓰면, 앞 테스트가 남긴 데이터가 뒤 테스트를 망칩니다. 단독으로는 통과하는데 전체로 돌리면 실패하는 것이 대표적인 신호입니다. Playwright 문서는 테스트마다 완전히 격리되어 독립적으로 실행되어야 재현성이 높아지고 실패가 연쇄되지 않는다고 강조합니다.
  2. 기다리는 방식이 틀렸다. 화면 요소가 늦게 나타나는데 "있는지"를 한 번만 확인하면 그 순간에 따라 결과가 갈립니다. Playwright의 웹 우선 단언(await expect(locator).toBeVisible())은 조건이 충족될 때까지 기다리며 재시도하지만, isVisible()로 직접 확인하면 기다리지 않고 바로 결과를 돌려줍니다. 고정 시간만큼 멈추는 방법은 느린 환경에서는 여전히 실패하고 빠른 환경에서는 시간을 낭비합니다.
  3. 외부에 의존한다. 실제 외부 API나 현재 시각에 따라 결과가 달라집니다. 테스트에서는 응답과 시각을 고정합니다.

재시도 설정은 증상을 가릴 뿐 원인을 없애지 않습니다. CI에서 가끔 실패하는 테스트는 실패 당시의 정보가 있어야 고칠 수 있으므로, Playwright 문서는 CI 실패 분석에 영상이나 캡처보다 trace viewer(기록해 둔 실행 과정을 단계별로 되짚어 보는 도구)를 쓰고, 실패한 테스트의 첫 재시도 때 trace를 남기도록 설정하라고 안내합니다.

내 컴퓨터와 다른 곳의 결과가 다를 때

"내 컴퓨터에서는 되는데요"는 개발 현장에서 가장 오래된 농담입니다. 결과가 다르면 두 환경의 차이가 원인입니다. 차이는 대개 다음 중 하나입니다.

차이 예 맞추는 법
런타임 버전 내 컴퓨터와 CI의 Node.js 주 버전이 다름 버전 파일을 두고 양쪽이 같은 버전을 쓰게 한다
설치된 의존성 npm install로 다른 하위 버전이 설치됨 lock 파일을 커밋하고 CI에서는 npm ci
환경 변수 값이 내 컴퓨터의 로컬 파일에만 있음 필요한 이름을 문서화하고 CI·배포 설정에도 넣는다
운영체제 macOS의 APFS는 대소문자 구분 형식을 따로 고르지 않으면 파일 이름의 대소문자를 구분하지 않지만, Linux는 구분한다 import 경로의 대소문자를 실제 파일 이름과 정확히 맞춘다
실행 방식 개발 서버에서만 확인하고 배포용 빌드는 안 돌림 배포 전에 빌드 명령을 로컬에서도 돌린다

환경 변수에는 특히 함정이 많습니다. Next.js 문서에 따르면 .env*.local 파일은 .gitignore로 저장소에서 빼는 것이 원칙이고, 테스트 환경(NODE_ENV=test)에서는 .env.local을 읽지 않습니다. 모두의 테스트가 같은 결과를 내게 하려는 설계라, 테스트가 내 .env.local의 값에 기대고 있다면 다른 곳에서 실패하는 것이 정상입니다.

성능도 같습니다. 개발 서버는 배포용 빌드와 다르게 코드를 실행하므로, 고치기 전과 후를 배포에 가까운 같은 조건에서 재야 비교가 됩니다.

실무 장면

소금빵집: 저장 안 되는 오전 9시

동네 빵집 '소금빵집'의 예약 앱에서 손님이 "예약 시간을 골라도 저장이 안 된다"고 알려 왔다. 운영자 민재는 고치기 전에 증상부터 재현했다. 오후 시간대를 고르면 저장되지만 오전 9시대를 고르면 저장 버튼을 눌러도 아무 일이 없었다. 개발자 도구의 콘솔은 조용했고, 네트워크 탭에는 요청이 나갔다가 400으로 돌아온 기록이 있었다. 화면이 아니라 요청 내용의 문제였다.

민재는 재현 절차를 에이전트에게 넘기며 "이 증상을 잡는 테스트를 먼저 추가하고, 고치기 전에 실패하는지 보여 줘"라고 했다. 에이전트는 곧바로 형식 검사 코드를 고치자고 제안했지만 민재는 거절했다. 실패를 먼저 보지 않으면 고친 뒤의 통과를 믿을 수 없었다. 테스트는 예상대로 실패했고 출력에는 Expected: "09:00" / Received: "9:00"이 찍혔다. 시간 문자열의 앞자리 0이 빠져 서버의 형식 검사에 걸리고 있었다.

에이전트가 시간을 만드는 함수를 고치자 새 테스트가 통과했다. 에이전트는 "완료"라고 보고했지만 민재는 거기서 멈추지 않았다. 전체 테스트와 타입 검사를 돌렸고, 브라우저에서 오전 9시를 골라 저장한 뒤 네트워크 탭에서 요청이 200으로 성공한 것과 새로고침 뒤에도 예약이 남아 있는 것을 봤다. 그제야 커밋했다.

재현이 있었기에 테스트를 쓸 수 있었고, 수정 전 실패를 봤기에 그 테스트를 믿을 수 있었으며, 브라우저 확인이 있었기에 증상이 사라졌다고 말할 수 있었다.

초록별 식물원: 오류 47개 앞에서

회원용 식물 관리 앱을 운영하는 '초록별 식물원'의 개발자 지호는 에이전트에게 흩어진 식물 도우미 함수들을 src/lib/plants/ 폴더로 정리해 달라고 맡겼다. 작업 뒤 npm run build를 돌리자 오류가 47개 쏟아졌다.

에이전트는 두 가지를 제안했다. 하나는 오류가 난 줄마다 타입 검사를 건너뛰는 주석을 다는 것이었고, 다른 하나는 폴더 정리를 통째로 되돌리는 것이었다. 지호는 둘 다 거절했다. 첫째 안은 오류를 덮을 뿐 원인을 그대로 남기고, 둘째 안은 요청한 정리 작업까지 버리기 때문이었다.

지호는 출력을 처음부터 읽었다. 맨 처음 오류는 src/app/watering/page.tsx에서 Cannot find module '@/lib/plant-utils'였다. 그 뒤의 오류 대부분은 이 모듈에서 가져오던 함수와 타입을 찾지 못해, 그것을 쓰는 파일마다 이어서 난 연쇄 오류로 보였다. 지호는 가설을 하나로 세웠다. "옛 경로를 가져오는 파일이 아직 남아 있다." 그리고 그 가설만 확인하도록 맡겼다.

옛 경로 '@/lib/plant-utils'를 가져오는 파일을 모두 찾아 새 경로로 바꿔라.
다른 코드는 바꾸지 마라. 바꾼 뒤 npm run build 를 다시 돌려 출력 전체를 보여 줘.

다시 돌린 빌드에서 오류는 두 개로 줄었다. 남은 두 개는 다른 원인이었다. 정리 과정에서 물 주기 계산 함수가 계절 값을 추가로 받도록 바뀌었는데, 호출하는 두 곳이 예전 방식 그대로였다. 지호는 이 변경이 요청 범위였는지 따졌다. 폴더 정리만 맡겼으니 함수의 모양을 바꾼 것은 범위 밖이었다. 함수를 원래 모양으로 돌려놓게 하자 빌드가 통과했다.

지호는 이어서 타입 검사와 테스트를 돌렸고, 브라우저에서 물 주기 일정 화면을 열어 날짜가 전과 같은지 확인했다. 47개의 오류는 원인 두 개였다. 첫 오류에서 시작해 가설을 하나씩 확인한 덕분에, 덮지도 버리지도 않고 고칠 수 있었다.

자주 틀리는 지점

  1. "테스트가 통과했으니 끝났다." 통과는 끝처럼 보입니다. 하지만 테스트는 테스트가 다루는 범위만 확인합니다. 화면을 바꿨는데 화면 테스트가 없다면 브라우저 확인 전에는 끝이 아닙니다.
  2. "실패하는 테스트는 기대값을 고치면 된다." 숫자 하나만 바꾸면 되니 빠릅니다. 하지만 기대값이 요구 사항과 같다면 틀린 쪽은 코드입니다. 에이전트가 테스트를 고치거나 건너뛰자고 할 때가 가장 주의할 순간입니다.
  3. "다시 돌려서 통과하면 괜찮다." 재실행이 통과하면 안심이 됩니다. 하지만 가끔 실패하는 데는 대개 상태 공유나 잘못된 기다림 같은 원인이 있고, 매번 같은 값으로 실패한다면 로직 오류입니다. 재실행은 원인 분석을 대신하지 못합니다.
  4. "오류가 많으면 문제가 많은 것이다." 숫자가 크면 막막해 보입니다. 하지만 수십 개의 오류가 원인 하나에서 연쇄된 경우가 많습니다. 맨 처음 오류부터 봅니다.
  5. "에이전트가 확실하다고 했다." 자신 있는 보고는 믿음을 줍니다. 하지만 자신감은 증거가 아닙니다. 명령, 출력, 화면 같은 증거를 요구하고, 확인하지 못한 것이 무엇인지 묻습니다.
  6. "대화를 지우면 작업도 처음으로 돌아간다." 새 세션은 새 출발처럼 느껴집니다. 하지만 맥락을 비워도 디스크의 코드 변경은 남습니다. 되돌릴지는 Git으로 따로 정합니다.

한눈에 정리

주제 기억할 것
오류의 자리 화면은 콘솔, 요청은 네트워크 탭, 서버는 터미널·로그
포트 충돌 EADDRINUSE는 포트를 이미 누가 쓰는 것. PID를 확인해 그것만 끈다
처음 실행 README → 런타임 버전 → npm ci → 환경 변수 → 마이그레이션 → 실행
검사 도구 테스트는 동작, 타입은 형식, 린트는 패턴, 빌드는 배포용 조립
오류 읽기 무엇·어디서·왜. 내 코드의 가장 위 줄, 맨 처음 오류, 종료 코드
원인 좁히기 재현 → 단계 가리기 → 첫 오류 → 가설 하나 → 확인 → 같은 검사 재실행
강제 장치 지시 < 지침 파일 < 훅 < CI 필수 검사 순으로 강하다
브라우저 흐름 전체, 좁은 화면, 새로고침 뒤 유지, 콘솔과 상태 코드
증거 명령과 원본 출력, 캡처, 확인하지 못한 것까지
맥락 정리 두 번 넘게 교정해도 안 되면 /clear와 더 나은 지시. 코드는 따로 되돌린다

공식 문서로 확인하기

셀프 체크

2-1. 개발 서버를 켜자 Error: listen EADDRINUSE: address already in use :::5173이 나왔다. 무슨 뜻이고, 어떻게 대응하는가? 대응할 때 피해야 할 방법도 하나 들라.

2-2. 에이전트가 "테스트 실행 결과 실패 0건"이라고 보고했다. 출력을 열어 보니 Tests: 0 passed, 0 total이었다. 이 결과를 통과로 받아들이면 안 되는 이유와 먼저 의심할 원인은 무엇인가?

2-3. 문의 폼의 '보내기' 버튼을 눌러도 아무 반응이 없다. 브라우저 개발자 도구의 콘솔과 네트워크 탭에서 각각 무엇을 보고, 그 결과로 문제의 위치를 어떻게 가르는가?

2-4. 다음 출력에서 가장 먼저 열어 볼 파일과 줄은 어디이고, 그 이유는 무엇인가?

TypeError: Cannot read properties of null (reading 'price')
    at sumLines (src/checkout/summary.ts:47:12)
    at OrderPanel (src/components/OrderPanel.tsx:18:9)
    at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:15486:18)

2-5. 빌드 명령의 출력에 노란 경고가 수십 줄 찍혔고, 마지막 줄에는 아무 오류 문구도 없다. 이 명령이 성공했는지 가장 확실하게 확인하는 방법은 무엇인가?

2-6. 에이전트가 할인 계산 버그를 고치면서 새 테스트를 하나 추가했고, 그 테스트는 통과한다. 이 테스트가 정말 그 버그를 잡는지 확인하는 간단한 방법을 설명하라.

2-7. 맥북에서는 잘 되던 앱이 Linux에서 도는 CI에서만 Module not found: Can't resolve './components/header'로 실패한다. 실제 파일 이름은 Header.tsx다. 원인은 무엇이고 어떻게 고치는가?

실습 과제

실습

실패 출력으로 한 바퀴 (45분)

  1. 연습용 앱에서 함수 하나에 일부러 버그를 넣는다(예: 합계에서 마지막 항목을 빼먹게).
  2. 그 버그를 잡는 테스트를 먼저 쓰고, 실패하는 것을 직접 확인한다.
  3. 실패 출력에서 무엇이·어디서·왜를 종이에 적는다.
  4. 출력 전체를 에이전트에게 붙여 주고 "원인을 고치고, 테스트는 바꾸지 말고, 전체 테스트 출력과 종료 코드를 보여 줘"라고 맡긴다.
  5. 고친 코드를 잠시 되돌려 테스트가 다시 실패하는지 확인한 뒤 원래대로 돌린다.

완료 기준: 내가 적은 원인과 에이전트가 고친 위치가 일치하고, 수정 전 실패 출력과 수정 후 통과 출력(종료 코드 0)이 함께 남아 있으며, 테스트 파일에는 변경이 없다.

실습

검증을 장치로 고정하기 (50분)

  1. 지침 파일에 작업 종료 때 돌릴 검사 명령(예: npm run check)을 한 줄 적는다.
  2. 쓰는 도구의 훅 기능으로 파일을 고칠 때마다 포매터나 린트가 돌게 설정하고, 그 설정 파일을 저장소에 커밋한다(Claude Code라면 .claude/settings.json의 PostToolUse).
  3. 에이전트에게 작은 화면 변경을 맡긴 뒤, 훅이 실제로 실행된 흔적을 확인한다.
  4. 브라우저에서 휴대전화 너비와 데스크톱 너비로 결과를 보고, 콘솔에 오류가 없는지와 네트워크 요청의 상태 코드를 기록한다.
  5. 완료 보고를 '실행한 검사, 화면 확인, 확인하지 못한 것' 세 칸으로 정리한다.

완료 기준: 커밋된 훅 설정이 있고, 에이전트가 따로 지시받지 않았는데도 훅이 돈 기록이 남아 있으며, 세 칸짜리 완료 보고에 '확인하지 못한 것' 칸이 비어 있지 않거나 비어 있는 이유가 적혀 있다.