forbidden code — fiction 03
금요일은 날코딩
팀 전체가 AI 코딩 도구를 끈다. 하루 동안.
누가 먼저 무너지고, 누가 가장 빛나는가.
Part I
"실험 설계"
2026년 3월 첫째 주. 1편 사건으로부터 3주 후.
진우의 "No AI Day" 슬랙 메시지가 예상치 못한 곳에서 반응을 얻었다. 외부 CTO 어드바이저 정윤호. 매주 화요일 오후에 2시간 출근하는, 회사에서 유일하게 넥타이를 매는 사람.
실험이 정식으로 설계됐다. CTO가 규칙을 정했다.
1. 금요일 09:00~18:00 모든 AI 도구 비활성화
(Claude, Copilot, ChatGPT, Cursor, AI 코드 리뷰)
2. IDE 자동완성(IntelliSense)은 허용. AI 제안은 금지
3. 각자 PR 최소 1개 올릴 것
4. 2주간 진행: 금AI(AI O), 금날(AI X) 교대
5. 측정 지표: 코드 양, 버그 수, 리뷰 시간, 팀 대화량
첫 번째 No AI Friday: 이번 주 금요일
금요일 아침. 8시 55분. 진우가 먼저 사무실에 도착했다. 모니터를 켰다. 터미널을 열었다. 습관적으로 claude를 치려다 멈췄다. 오늘은 No AI Friday다.
서연이 출근했다. 한결이 출근했다. 9시 정각. CTO의 실험이 시작됐다.
Part II
금단 증상 보고서
오전 9시 ~ 오후 1시. 4시간의 기록.
09:00 — 박진우
진우는 비교적 괜찮았다. 3주 전의 29시간 서바이벌이 예방접종이 된 것이다. 오늘 태스크는 대시보드 API에 캐시 레이어를 추가하는 것. Redis를 쓸지 인메모리를 쓸지는 이미 정했다. Redis. 이유도 안다 — 서버가 재시작해도 캐시가 유지되어야 하니까.
문제는 에러 핸들링 패턴이었다. 평소에는 Claude에게 "Redis 커넥션 실패 시 폴백 패턴 짜줘"라고 하면 try-catch 구조가 나왔다. 오늘은 직접 짜야 한다.
async def get_dashboard_data(self, user_id: str):
try:
cached = await self.redis.get(f"dash:{user_id}")
if cached:
return json.loads(cached)
except RedisConnectionError:
logger.warning("Redis unavailable, skipping cache")
# Redis 실패 시 DB 직접 조회
data = await self.repo.get_dashboard(user_id)
try:
await self.redis.setex(
f"dash:{user_id}", 300, json.dumps(data)
)
except RedisConnectionError:
pass # 캐시 저장 실패는 무시
return data
간단한 코드다. 하지만 이 코드를 쓰는 데 30분이 걸렸다. RedisConnectionError가 정확히 어떤 클래스인지 redis-py 문서를 찾아보고, setex의 인자 순서를 확인하고, TTL 300초가 대시보드에 적절한지 생각했다. Claude였으면 3분이었을 것이다. 하지만 진우는 TTL을 왜 300으로 했는지 설명할 수 있다. 대시보드 데이터가 5분 이상 지연되면 사용자가 불만을 느끼기 시작하는 지점이니까.
3주 전에는 awk 명령어 하나 치는 데 20분 걸렸는데. 지금은 30분에 캐시 레이어를 짠다. 근육이 돌아오고 있다.
09:00 — 윤한결
한결은 가장 심각한 금단 증상을 보였다.
오늘 태스크는 모바일 반응형 레이아웃 수정. 프로필 카드가 768px 이하에서 깨지는 문제. CSS를 직접 써야 한다.
.profile-card {
display: flex;
justify-content: center; /* 이게 맞나? 가로 정렬? */
align-items: center; /* 세로 정렬? 가로 정렬? */
}
justify-content와 align-items. 한결은 이 둘의 차이를 매번 헷갈린다. flex-direction이 row면 justify-content가 가로고 align-items가 세로다. column이면 반대다. 왜 반대인가? 주축(main axis)과 교차축(cross axis)이라는 개념 때문이다. 한결은 이것을 이번에 처음 이해했다.
한 시간 동안 작성한 CSS: 3줄. MDN에서 Flexbox 가이드를 처음부터 읽고 있었다.
AI한테 "이 카드 가운데 정렬해줘"라고 하면 5초면 됐는데. 5초짜리 일을 1시간째 하고 있다.
11시. 한결은 flexbox를 포기하고 grid로 바꿨다. 그런데 grid는 더 모른다.
.profile-card {
display: grid;
grid-template-columns: 80px 1fr;
gap: 16px;
align-items: start;
}
@media (max-width: 768px) {
.profile-card {
grid-template-columns: 1fr;
text-align: center;
}
}
이 코드를 쓰는 데 40분이 걸렸다. MDN의 CSS Grid 가이드를 읽으면서, grid-template-columns가 컬럼 너비를 정의한다는 것을 알았다. 1fr이 "남은 공간을 차지한다"는 뜻이라는 것을 알았다. 미디어 쿼리에서 1fr로 바꾸면 1열 레이아웃이 된다는 것을 알았다.
12시. 한결이 드디어 깨달은 것.
아... 그래서 2차원은 grid고 1차원은 flex인 거구나. grid는 행과 열을 동시에 제어하고, flex는 한 방향만 제어한다. 대학에서 과제 낼 때 이 차이를 몰랐다. AI가 알아서 골라줬으니까.
09:00 — 이서연
서연은 유일하게 평소와 비슷한 속도였다. 서연에게 AI는 선택적 도구였다. 코드를 짤 때는 직접 쓰고, 반복적인 작업에만 AI를 활용했다.
하지만 서연에게도 비밀이 있었다.
오늘 태스크는 결제 완료 페이지의 리팩토링. 서연이 코드를 짜고, 테스트를 쓰려고 테스트 파일을 열었다. 빈 파일이 아니었다. 지난주 AI가 짜준 테스트가 이미 있었다.
테스트는 AI한테 시키고 있었다. 코드는 직접 짜지만, 테스트는 지루하니까. "이 컴포넌트의 엣지 케이스 테스트 짜줘." AI가 테스트를 생성하면 나는 돌리기만 했다. 테스트가 통과하면 넘어갔다.
오늘은 테스트도 직접 써야 한다. 서연은 Jest 파일을 열고 타이핑을 시작했다.
describe('PaymentComplete', () => {
it('결제 금액이 0원이면 에러 메시지를 표시한다', () => {
// 잠깐... 결제 금액이 0원인 경우가 있나?
// 쿠폰 100% 할인이면 0원 결제가 가능하다.
// 그때 완료 페이지에 "0원"이 표시되면?
// UI에 "결제 완료: 0원" — 이건 맞는 건가?
});
});
서연은 테스트를 쓰다가 자기 코드의 엣지 케이스를 발견했다. 결제 금액 0원. 쿠폰 100% 할인 시 발생하는 케이스. UI에서 "결제 완료: 0원"이 표시된다. 이것이 의도된 동작인가? 기획서에는 이 케이스가 없다. AI가 테스트를 짤 때는 이런 질문을 하지 않았다. AI는 코드의 동작을 테스트했지, 코드의 의도를 질문하지 않았다.
"진우야. 쿠폰 100% 할인이면 결제 금액 0원이 가능한 거 맞지?"
"어. 가능해. 왜?"
"결제 완료 페이지에서 0원이 그대로 표시되는데, 이거 의도된 거야?"
진우가 멈칫했다. "그건 기획에서 빠진 케이스인데."
"AI한테 테스트 시키면 이런 거 안 물어봐. 코드가 동작하는지만 확인하지, 기획이 맞는지는 안 물어보거든."
10:30 — 김대표
진우는 "실험이라서 느린 건 아닙니다"라고 답했지만, 실험이라서 느린 것이 맞았다. 정확히는, 평소에는 AI가 대신 고민해주던 것을 오늘은 자기가 직접 고민하고 있으므로 느린 것이었다.
Part III
오후의 반전
오후 2시. 예상 밖의 일이 벌어지기 시작했다.
진우가 올린 캐시 레이어 PR을 서연이 리뷰했다. 버그를 발견했다. RedisConnectionError 이외의 예외 — 예를 들어 직렬화 실패 — 가 처리되지 않았다. 하지만 에러 메시지가 명확했다.
"진우야. json.dumps에서 datetime 객체가 직렬화 안 되는 케이스 있어."
"어디?"
"get_dashboard 반환값에 last_login 필드가 datetime이야. json.dumps가 터지는데, 에러 메시지가 정확해서 바로 찾았어."
진우가 고쳤다. json.dumps에 default=str을 추가하는 한 줄. 5분 만에 끝났다.
AI가 짰으면 이 에러가 안 났을 수도 있다. 하지만 AI가 짰으면 에러 메시지가 generic했을 것이다. "Internal Server Error." 그러면 원인을 찾는 데 5분이 아니라 30분이 걸렸을 것이다.
진우의 코드는 AI의 코드보다 버그가 하나 더 있었다. 하지만 버그의 원인이 명확했고, 수정이 빨랐다. AI의 코드는 버그가 적지만, 버그가 있을 때 원인을 찾기 어렵다. 에러 핸들링이 과도하게 추상화되어 있어서 어디서 터졌는지 추적하기 어려운 것이다.
오후 3시. 한결에게도 반전이 있었다.
4시간 동안 CSS와 씨름한 한결이 드디어 모바일 레이아웃을 완성했다. grid로 2열 레이아웃을 만들고, 미디어 쿼리로 1열로 전환하고, 아바타 이미지의 object-fit을 적용했다. 그리고 브라우저 개발자 도구에서 반응형 모드를 열어 직접 확인했다.
"서연 선배. 이거 한번 봐주세요."
서연이 한결의 모니터를 보았다. 프로필 카드가 768px에서 깔끔하게 1열로 전환되고 있었다.
"한결 씨, 이거 직접 짠 거 맞지?"
"네. 4시간 걸렸어요."
"잘했네. 근데 하나 물어볼게. grid-template-columns: 80px 1fr에서 왜 80px?"
"아바타 크기가 64px이고, 패딩 8px 양쪽이면 80px이니까요."
서연이 웃었다. "대답할 수 있네. 지난번이랑 다르다."
오후 4시. 팀이 모여서 코드 리뷰를 했다. 평소와 다른 점이 하나 있었다. 대화가 많았다.
AI가 있을 때는 각자 조용히 코딩했다. 모르는 게 있으면 AI에게 물었다. 동료에게 물어보는 것보다 AI에게 물어보는 것이 빨랐고, 부담이 없었다. 하지만 오늘은 AI가 없다. 모르는 게 있으면 동료에게 물어야 한다.
"서연 선배, Jest에서 async 컴포넌트 테스트할 때 waitFor 쓰는 게 맞아요?"
"맞아. findBy도 되는데, waitFor가 더 명시적이야."
"진우 선배, Redis setex랑 set에 ex 옵션 주는 거랑 뭐가 달라요?"
"같은 건데 setex는 Redis 2.0 호환이라 레거시야. set에 ex 쓰는 게 낫다. 내 코드 고쳐야겠네."
진우가 한결의 질문 덕에 자기 코드의 개선점을 찾았다. 이런 일은 AI와의 대화에서는 일어나지 않는다. AI는 질문에 답하지, 역으로 질문하지 않는다.
AI에게 물으면 답이 온다
동료에게 물으면 질문이 돌아온다
오후 5시. 서연이 테스트를 직접 쓰면서 발견한 것들을 정리했다.
"테스트를 직접 쓰니까 이상한 게 보여. AI한테 시키면 AI가 코드의 동작을 테스트하거든. '이 함수를 호출하면 이 값이 나온다.' 근데 직접 쓰면 동작이 아니라 의도를 테스트하게 돼. '이 결과가 사용자한테 맞는 건가?'"
서연이 오늘 발견한 엣지 케이스 목록: 결제 금액 0원 표시, 결제 실패 시 뒤로가기 무한 루프, 영수증 PDF에 회사 이름 누락. 세 가지 모두 AI가 짜준 테스트에서는 잡히지 않았던 것들이다. AI는 코드가 동작하는지 확인했지, 코드가 올바른지는 묻지 않았다.
Part IV
월요일의 데이터
2주 후. 월요일 오전. 회의실.
CTO 정윤호가 2주간의 A/B 데이터를 화면에 띄웠다. 금요일 2회(AI 사용) vs 금요일 2회(AI 미사용).
| metric | ai friday | no ai friday | diff |
|---|---|---|---|
| 코드 양 (줄) | 평균 340줄/인 | 평균 220줄/인 | -35% |
| PR 버그 (리뷰 후 발견) | 평균 3.2건/PR | 평균 1.8건/PR | -44% |
| 코드 리뷰 시간 | 평균 42분/PR | 평균 22분/PR | -48% |
| 팀 대화 (슬랙 메시지) | 평균 18건 | 평균 54건 | +200% |
| 기획 불일치 발견 | 0건 | 3건 | +3 |
회의실이 조용했다. 김대표가 먼저 입을 열었다.
"코드 양이 35% 줄었잖아."
"맞습니다." CTO가 답했다. "하지만 버그가 44% 줄었고, 리뷰 시간이 48% 줄었습니다. 코드가 적은 게 아니라 불필요한 코드가 없는 겁니다."
진우가 보충했다.
"AI가 짜면 방어적 코드가 많아요. try-catch가 중첩되고, 타입 체크가 과도하고, 에러 핸들링이 추상화돼 있어요. 코드 양은 많은데 에러가 터지면 추적이 어렵죠. 직접 짜면 코드가 적지만 에러 메시지가 구체적이에요."
CTO가 또 하나의 데이터를 가리켰다.
"팀 대화량이 3배입니다. AI가 없으니까 서로 묻는 겁니다. 그리고 이 대화에서 기획 불일치 3건을 찾았습니다. AI에게 물으면 코드 답이 오지만, 동료에게 물으면 기획 답이 옵니다."
서연이 덧붙였다.
"테스트도요. AI가 짜면 코드의 동작을 테스트하는데, 직접 쓰면 코드의 의도를 테스트하게 돼요. 이번에 결제 금액 0원 케이스를 찾은 게 그 예시예요."
김대표가 팔짱을 끼고 물었다.
"투자자한테 '우리는 일주일에 하루 AI를 안 씁니다'라고 하면 뭐라고 생각하겠어."
CTO가 대답했다.
"'우리 팀은 AI를 쓸 때와 안 쓸 때의 차이를 데이터로 측정했고, 그 결과를 바탕으로 AI 사용 전략을 최적화했습니다.' 투자자가 싫어할 말은 아닙니다."
METR 연구. 숙련 개발자 16명, 246개 태스크. AI를 쓴 그룹이 19% 느렸다. 하지만 본인들은 20% 빨라졌다고 느꼈다. 인식과 현실의 괴리 40%포인트. 이 팀의 데이터도 비슷했다. 코드 양은 줄었지만, 품질은 올라갔다.
CTO가 제안했다.
"격주 금요일을 'No AI Friday'로 정식 운영하자. 강제가 아니라 선택. 하지만 참여자는 데이터를 기록해줘."
김대표가 한숨을 쉬며 고개를 끄덕였다.
"좋아. 대신 고객 이슈 있으면 AI 써."
"당연하죠."
회의가 끝나고 자리로 돌아가는 길. 한결이 진우에게 말했다.
"진우 선배. 다음 금요일도 No AI죠?"
"당연하지."
"이번엔 CSS 4시간 안 걸릴 거예요."
진우가 웃었다.
다음 금요일. 한결이 아침에 사무실에 들어오며 말했다.
"오늘은 No AI Friday잖아요."
진우와 서연이 동시에 고개를 돌렸다. 한결이 먼저 말한 것은 처음이었다. 2주 전에 "... 열심히 하겠습니다"라고 작게 말했던 신입이, 오늘은 먼저 No AI Friday를 선언했다.
서연이 커피를 건네며 말했다.
"한결 씨, 오늘 태스크 뭐야?"
"결제 완료 페이지 리디자인이요. 서연 선배가 찾은 0원 케이스 처리도 포함해서."
"기획팀이랑 얘기했어?"
"네. 어제 슬랙으로 확인했어요. 0원 결제는 '무료 결제 완료'로 표시하기로 했습니다."
서연이 한결을 보았다. 2주 전 코드 리뷰에서 useCallback을 왜 넣었는지 대답하지 못했던 신입이, 이제 기획팀에 먼저 확인하고 온다. 코드를 짜기 전에 의도를 확인한다.
이것이 AI가 할 수 없는 일이다. AI는 코드를 짠다. 하지만 "이 결과가 사용자한테 맞는 건가?"라는 질문은 하지 않는다. 그 질문은 사람만 할 수 있다.
코드 양은 35% 줄었다
버그는 44% 줄었다
AI에게 물으면 답이 온다. 동료에게 물으면 질문이 돌아온다. 그 질문이 기획의 빈틈을 찾는다.