어제 고친 버그를 오늘 또 만드는 AI

결론부터 말하면, AI는 똑똑하지만 기억력이 없다. 그래서 삽질에서 얻은 교훈은 사람이 아니라 파일에 남겨야 한다.

1년 전 Claude Code로 개발에 복귀한 뒤 CodeIgniter 4 기반 솔루션 7개를 혼자 만들었다. 속도는 확실히 빨라졌다. 그런데 이상한 반복이 있었다. 어제 한 시간 걸려 잡은 문제를, 새 세션의 Claude가 오늘 똑같이 다시 만든다.

세션이 바뀌면 대화 맥락은 사라진다. 사람 동료라면 "아, 그거 지난번에 데였지" 하고 피해 갈 일을, AI는 매번 처음 보는 얼굴로 다시 밟는다. 지난 글에서 CLAUDE.md로 팀 규칙을 가르치는 법을 다뤘다면, 이번 글은 그 반대편 이야기다. 규칙이 아니라 흉터, 즉 삽질 교훈을 세션 간에 이어 붙이는 법이다.

이 글에서 예로 드는 교훈은 두 가지다.

  • .env 값은 따옴표로 감싼다
  • error_log() 출력은 믿지 않는다

둘 다 사소해 보이지만, 모를 때는 반나절을 날리고 알고 나면 한 줄로 끝나는 종류의 지식이다. 메모리 파일이 가장 큰 효과를 내는 게 바로 이런 지식이다.

규칙은 CLAUDE.md, 흉터는 MEMORY.md

Claude Code가 세션을 넘어 기억하는 통로는 크게 두 가지다. 내가 쓰는 CLAUDE.md, 그리고 Claude가 스스로 쓰는 auto memory다.

구분

CLAUDE.md

auto memory (MEMORY.md)

누가 쓰나

사람

Claude (사람이 지시·수정 가능)

위치

프로젝트 루트 ./CLAUDE.md, 전역 ~/.claude/CLAUDE.md

~/.claude/projects/<프로젝트>/memory/

공유

저장소에 커밋, 팀과 공유

내 머신에만 존재

로딩

세션 시작 시 전체

세션 시작 시 MEMORY.md 앞부분(약 200줄), 세부 파일은 필요할 때

담을 것

아키텍처, 코딩 규약, 금지 구역

실제로 겪은 함정, 교정받은 습관, 환경 특이사항

공식 문서도 이 둘을 같은 맥락에서 구분한다. CLAUDE.md는 Claude의 행동을 이끌기 위한 것이고, auto memory는 내가 고쳐준 내용을 Claude가 따로 수고 없이 배우게 하는 장치다. (Claude Code 공식 문서 - memory)

구조는 단순하다. MEMORY.md는 한 줄짜리 포인터를 모아둔 목차이고, 자세한 내용은 debugging.md, env.md 같은 주제별 파일로 나뉜다. 매 세션에는 목차만 올라오고, 주제 파일은 관련 작업을 할 때 열린다. 컨텍스트를 아끼면서도 "이런 함정이 있었다"는 존재 자체는 항상 알고 있게 되는 구조다.

~/.claude/projects/<프로젝트>/memory/
├── MEMORY.md      # 목차, 매 세션 로딩
├── env.md         # .env 관련 함정
└── debugging.md   # 로그·디버깅 관련 함정

내 기준으로 정리하면 이렇다. "이렇게 짜라"는 CLAUDE.md, "여기서 데었다"는 MEMORY.md. 규칙은 팀과 함께 지키는 것이고, 흉터는 내 환경에서 실제로 난 상처라 남과 공유할 필요가 없는 경우가 많다.

교훈 1: .env 값은 따옴표로 감싼다

결론: CI4 .env의 문자열 값은 무조건 큰따옴표로 감싸게 했고, 그 뒤로 이 종류의 버그는 다시 나지 않았다.

CI4 프로젝트를 여러 개 세팅하다 보면 DB 비밀번호, API 키, 앱 이름 같은 값을 .env에 계속 넣게 된다. 문제는 따옴표 없이 쓴 값이 대부분은 멀쩡히 동작한다는 점이다. 그래서 Claude도, 나도 습관처럼 따옴표를 생략한다. 그러다 특정 값에서만 터진다.

# 겉보기엔 멀쩡하지만
database.default.password = p@ss#2026
app.name = AIvance Admin

# 이렇게 써야 안전하다
database.default.password = "p@ss#2026"
app.name = "AIvance Admin"

따옴표가 없으면 CI4의 DotEnv 파서는 값을 이렇게 처리한다.

  • 공백이 있는 값: 따옴표로 감싸라는 예외를 던지며 부팅 자체가 실패한다. 차라리 이건 낫다. 바로 보이니까.
  • #이 들어간 값: # 뒤를 주석으로 보고 잘라낼 수 있다. 비밀번호가 조용히 짧아지고, 증상은 "DB 접속 거부"로만 나타난다. 이게 진짜 무섭다.
  • $가 들어간 값: ${변수} 형태는 다른 환경변수로 치환된다. 의도치 않은 값이 들어갈 여지가 생긴다.

특수문자가 섞인 비밀번호는 셸에서도, SQL에서도, .env에서도 각각 다른 방식으로 사람을 괴롭힌다. 이 함정은 한 번 겪고 나면 원인이 뻔한데, 겪기 전에는 "비밀번호를 잘못 넣었나"부터 의심하게 된다.

메모리에 남긴 형태

# env.md

## .env 값은 항상 큰따옴표
- 규칙: CI4 .env의 문자열 값은 "…"로 감싼다. 숫자·불리언만 예외.
- 이유: 따옴표 없으면 공백은 부팅 실패, `#`은 뒤가 주석으로 잘림,
  `${}`는 치환됨. # 잘림은 에러 없이 "DB 접속 거부"로만 보인다.
- 확인법: php spark env 대신 env('키')를 직접 찍어 길이까지 비교.
- 출처: 2026 AIvance 서버 세팅 중 DB 비밀번호에서 발생.

핵심은 규칙, 이유, 증상, 확인법을 함께 적는 것이다. 규칙만 적으면 Claude는 "이 값은 특수문자가 없으니 괜찮겠지" 하고 예외를 만든다. 증상까지 적어두면, 나중에 "DB 접속 거부"를 만났을 때 Claude가 먼저 .env를 의심한다.

교훈 2: error_log()는 믿지 않는다

결론: "로그가 안 찍혔다"는 "코드가 실행되지 않았다"는 증거가 아니다. 이걸 메모리에 박아둔 뒤로 Claude의 헛다리 추론이 확 줄었다.

Claude Code에게 디버깅을 맡기면 흔히 이런 흐름이 나온다.

  1. 의심 지점에 error_log('여기 도착')을 넣는다
  2. 요청을 보낸다
  3. 로그 파일을 열어보니 아무것도 없다
  4. "이 코드는 실행되지 않습니다. 라우팅 문제로 보입니다"라고 결론 내린다
  5. 멀쩡한 라우트를 고치기 시작한다

4번이 함정이다. error_log()의 출력은 실행 경로마다 다른 곳으로 간다.

실행 환경

error_log() 출력 위치

Apache + mod_php

Apache 에러 로그 (vhost별로 다를 수 있음)

PHP-FPM

FPM 풀 설정에 따라 FPM 로그 또는 버려짐

php spark serve / CLI

터미널 stderr

CI4 log_message()

writable/logs/ — 단, 임계값(threshold) 아래면 기록 안 됨

로컬은 macOS + Apache, 서버는 Ubuntu, 테스트는 CLI로 돌리다 보니 같은 코드의 로그가 세 군데로 흩어졌다. Claude는 자기가 연 파일 하나만 보고 "없다"고 판단한다. 거기에 writable/logs 쓰기 권한 문제나 production 환경의 로그 임계값까지 겹치면, 로그 부재는 거의 아무것도 증명하지 못한다.

메모리에 남긴 형태

# debugging.md

## error_log 출력 부재로 "미실행" 판단 금지
- 규칙: 로그가 없다는 이유만으로 코드가 실행 안 됐다고 결론 내리지 않는다.
- 이유: error_log는 Apache/FPM/CLI마다 출력 위치가 다르고,
  log_message는 threshold와 writable 권한에 걸린다.
- 대신: 실행 여부는 응답에 임시 헤더를 박거나, 테스트에서 단언하거나,
  file_put_contents로 절대경로 파일에 직접 기록해 확인한다.
- 디버깅 후 임시 코드는 반드시 제거.

여기서 중요한 건 "하지 마"가 아니라 "대신 이렇게"까지 적는 것이다. 금지만 적으면 Claude는 다른 그럴듯한 추측으로 같은 실수를 반복한다. 대안 확인법을 함께 주면, 다음 세션의 Claude는 로그가 비었을 때 결론 대신 확인 절차를 먼저 꺼낸다.

좋은 메모리 항목의 조건

두 교훈을 정리하면서 메모리 항목에도 품질 차이가 있다는 걸 알게 됐다. 쓸모 있는 항목은 다음 네 가지를 갖췄다.

  1. 한 줄 규칙 — 무엇을 하고, 무엇을 하지 않는지
  2. 이유 — 왜 그런지. 이유가 없으면 Claude는 "이번엔 예외"를 만든다
  3. 증상 — 이 함정에 빠졌을 때 겉으로 보이는 모습. 나중에 역추적의 단서가 된다
  4. 대안 또는 확인법 — 금지만 있으면 다른 실수로 우회한다

반대로 남기지 않는 게 나은 것도 있다.

  • 코드나 git 이력을 보면 바로 알 수 있는 사실
  • 이미 CLAUDE.md에 있는 규칙의 중복
  • 이번 작업에만 해당하는 임시 상태 ("지금 auth 브랜치 작업 중")
  • 감정이 섞인 기록 ("이거 진짜 짜증났음") — 정보가 없다

어디에 둘지 판단 기준

질문

예 →

아니오 →

팀 전체가 지켜야 하나?

CLAUDE.md

다음 질문

모든 프로젝트에 공통인가?

~/.claude/CLAUDE.md

다음 질문

이 프로젝트에서 실제로 겪은 함정인가?

auto memory

남기지 않는다

매번 기계적으로 막아야 하나?

훅(hook)으로 강제

메모리로 충분

마지막 줄이 중요하다. .env 따옴표 규칙처럼 검사 가능한 것은 결국 PreToolUse 훅으로 옮기는 게 맞다. 메모리는 부탁이고 훅은 강제다. 메모리로 시작해서, 같은 실수가 두 번 나면 훅으로 승격시키는 흐름이 내 환경에서는 잘 맞았다.

운영하면서 정한 습관

  • 삽질이 끝나면 바로 말한다. 문제를 해결한 직후 "방금 원인을 메모리에 남겨줘. 규칙, 이유, 증상, 확인법 형식으로"라고 한 줄 지시한다. 시간이 지나면 이유를 잊는다.
  • /memory로 주기적으로 열어본다. Claude가 스스로 적은 항목 중에는 과하게 일반화된 것도 있다. 한 달에 한 번 정도 훑어 틀린 건 고치고 낡은 건 지운다.
  • MEMORY.md는 목차로만 쓴다. 세션마다 앞부분만 올라오므로 상세 내용은 주제 파일로 빼고 목차는 한 줄씩 짧게 유지한다.
  • 두 번 나면 승격한다. 메모리에 있는데도 같은 실수가 반복되면 훅이나 테스트로 옮긴다.
  • 비밀값은 절대 적지 않는다. "비밀번호에 #이 있었다"는 적어도, 비밀번호 자체는 적지 않는다.

맺음말

25년 넘게 개발하면서 가장 비싼 지식은 늘 문서에 없는 것이었다. "그 서버는 원래 그래", "그 설정은 건드리면 안 돼" 같은 것들. 사람 팀에서는 이런 지식이 선배의 머릿속에 있었다. AI와 일하는 지금은 그 자리를 메모리 파일이 대신한다.

AI에게 기억을 심는다는 건 거창한 일이 아니다. 데인 자리마다 짧은 메모를 붙여두는 것이다. 그 메모가 쌓이면, 새 세션의 Claude도 처음부터 "아, 거기 조심해야죠"라고 말하기 시작한다.

참고