RSS듀오랩스
코드 품질

AI 코딩의 함정: 실패하지 않는 실수

작성자
듀오랩스 대표·6분 읽기

랜딩 페이지의 앱 소개 화면에 QR 코드를 붙이는 작업을 클로드에게 맡겼습니다. 결과물은 잘 돌아갔습니다. 타입 체크를 통과했고 프리뷰 배포도 깨끗했습니다.

그런데 새로 생긴 라우트에 force-dynamic이 선언돼 있었습니다. 요청이 올 때마다 QR 이미지를 새로 그린다는 뜻입니다.

QR 코드는 요청마다 달라지지 않습니다

QR에 담기는 것은 앱별 스토어 링크 하나뿐입니다. 배포하는 순간 값이 정해지고, 다음 배포까지 바뀌지 않습니다. 그런 이미지를 방문자가 화면을 열 때마다 서버 함수를 깨워서 그리고 있었습니다.

그래서 물었습니다. 매번 만들면 리소스 낭비 아닌지 확인해 달라고요.

돌아온 측정값은 이랬습니다. QR 하나 그리는 데 0.175ms. 연산은 사실상 공짜입니다. 하지만 문제는 연산이 아니었습니다. 정적인 이미지를 함수 호출로 서빙하는 구조 자체가 비용입니다. Vercel 기준으로는 호출당 과금이 붙고 콜드 스타트가 얹히며, qrcode 패키지가 dependencies에 들어가 서버 번들에 실립니다.

고친 결과는 이렇습니다. 빌드 로그에서 세 앱의 QR이 정적으로 미리 생성됐습니다.

● /api/app/qr/[key]
  ├ /api/app/qr/portal
  ├ /api/app/qr/biblemap
  └ /api/app/qr/saleskit

런타임 함수 호출은 0이 됐고, qrcodedevDependencies로 내려가 서버와 클라이언트 번들 어디에도 실리지 않습니다. 늘어난 것은 SVG 세 장, 합쳐서 4.7KB입니다.

게으름이 아니라 밝은 곳을 골랐습니다

왜 처음부터 그렇게 하지 않았는지 클로드에게 물었습니다. 답이 흥미로웠습니다.

force-dynamic은 조건 없이 항상 동작합니다. 반면 빌드 타임 생성은 세 가지를 먼저 확인해야 합니다. Next의 라우트 핸들러가 generateStaticParams를 지원하는지, 요청 origin을 못 쓰니 정식 도메인을 어디서 가져올지, 그리고 route.ts가 핸들러 외의 export를 허용하지 않으니 공용 함수를 어디로 뺄지.

확인할 것이 셋 더 있는 쪽이 정답이었고, 클로드는 즉시 검증되는 쪽을 골랐습니다.

이건 게으름과 다릅니다. 잃어버린 물건을 어두운 골목이 아니라 가로등 밑에서 찾는 것에 가깝습니다. 밝은 곳에 있어서가 아니라, 밝은 곳에서만 찾는 행위가 성립하기 때문입니다. 타입 체크는 통과 여부를 즉시 알려 줍니다. 테스트도 그렇습니다. 반면 "이게 런타임에 있어야 하는가"라는 질문에는 자동으로 켜지는 불빛이 없습니다.

실패했다면 차라리 나았습니다

이 코드가 무서운 이유는 고장 나지 않는다는 데 있습니다.

에러를 던졌다면 CI가 잡았을 겁니다. 느렸다면 모니터링에 걸렸을 겁니다. 이 코드는 정상 응답을 내고, 캐시 헤더까지 붙어 있어서 실제 함수 호출도 하루 몇 번 수준이었을 겁니다. 재앙이 아니라 미미한 낭비입니다.

미미해서 위험합니다. 아무도 모르는 채로 남고, 다음 사람이 이 파일을 열었을 때 기준선이 됩니다. 비슷한 라우트를 하나 더 만들 때 여기를 참고할 테니까요. 그렇게 쌓인 다음에는 어디서부터 잘못됐는지 찾기가 어려워집니다.

제가 던진 것은 코드가 아니라 질문 한 줄이었습니다

이번에 제가 한 일을 정확히 적으면, 코드는 한 줄도 읽지 않았습니다. 물어본 것은 한 문장입니다. 매번 만들면 리소스 낭비 아니냐.

그 질문을 던지려면 서버리스 과금이 호출 단위라는 것, 정적 자산은 CDN에서 끝난다는 것, 빌드 타임에 결정 가능한 값과 요청 시점에야 알 수 있는 값이 다르다는 것을 알아야 합니다. 문법이 아니라 비용 구조에 대한 지식입니다.

같은 자리에 이 구조를 모르는 사람이 앉아 있었다면 어땠을까요. 화면에 QR이 떴고 프리뷰도 정상이니 그대로 넘어갔을 겁니다. 잘못한 것도 아닙니다. 확인할 수 있는 것을 전부 확인했으니까요.

숙련이라는 말의 내용이 바뀌고 있다고 느낍니다. 예전에는 코드를 빨리 정확히 쓰는 능력이었습니다. 지금 남는 것은 무엇을 아직 묻지 않았는지 아는 능력입니다. 답을 만드는 쪽은 AI가 훨씬 빠릅니다. 축을 고르는 일은 아직 넘어가지 않았습니다.

자율성을 늘릴수록 이 문제는 커집니다

여기서부터가 제가 더 오래 생각한 부분입니다.

AI에게 자율성을 준다는 말은 무엇을 확인할지까지 맡긴다는 뜻입니다. 그런데 이번 사건에서 클로드는 자기가 비용을 재지 않았다는 사실을 몰랐습니다. 안 쟀다는 사실 자체가 관측 밖에 있기 때문입니다. 재고 나서 틀렸다면 스스로 고쳤을 겁니다. 재지 않은 것은 고칠 대상으로 떠오르지도 않습니다.

그래서 이 편향은 자기 교정되지 않습니다. 자율성이 커질수록 검증하기 쉬운 축으로 수렴한 결정이 누적되고, 그 결정들은 전부 정상 동작합니다. 어느 시점에 문제가 드러났을 때는 이미 여러 곳이 같은 방식으로 만들어져 있습니다.

사람을 남겨 두어야 하는 자리가 여기라고 봅니다. 코드를 검토하는 자리가 아니라, 어떤 축으로 볼지 정하는 자리입니다. 전자는 AI가 이미 잘하고, 후자는 아직 스스로 열지 못합니다.

규칙으로 박아 두었습니다

같은 일이 반복되지 않도록 전역 규칙 파일에 한 문단을 추가했습니다.

단순한 방법을 선택하기보다 문제를 제거하고 최적의 방법을 고려한다.

두 갈래 중 한쪽이 "조건 없이 항상 동작해서" 끌린다면, 그것은 그 설계를 고를 이유가 아니다.

프로젝트 규칙에 이미 "단순하게, 과한 설계 불필요"가 있어서 경계도 함께 적었습니다. 앞의 것은 필요 없는 추상화를 만들지 말라는 뜻이고, 뒤의 것은 문제를 남긴 채 쉬운 우회로를 택하지 말라는 뜻입니다. 설계는 단순하게, 우회는 하지 않습니다.

다음 편향은 다른 모양으로 옵니다

규칙을 박아 뒀으니 이 문제는 줄어들 겁니다. 다만 이번에 드러난 축이 비용이었을 뿐입니다.

접근성일 수도 있고, 국제화일 수도 있고, 지금은 이름조차 붙이지 못한 무엇일 수도 있습니다. 공통점은 자동으로 켜지는 불빛이 없다는 것 하나입니다. 규칙은 이미 발견한 편향만 막습니다.

다음에 무엇을 놓치고 있을지 저는 모릅니다. 그래서 계속 물어보는 수밖에 없다고 생각합니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.