AI도 휴식이 필요하대요
정해진 디자인 시스템 컴포넌트로만 화면을 생성하는 서비스를 만들며, 프롬프트를 '컴파일러'처럼 설계한 이야기. 모델이 코드 대신 엉뚱한 영문 기사를 뱉은 recitation 이탈을 추적하고, "이탈은 못 막아도 새어 나가는 건 막는다"는 출력 검증·관측 안전망으로 대응한 기록.
디자인 시스템을 코드로 그려내는 과정에서, 가끔 쉬어버리는 AI를 다루는 법
시작은 단순한 질문이었습니다
“AI한테 코드 짜달라는 거, 이제 다들 하잖아?”
맞습니다. “로그인 페이지 만들어줘” 하면 그럴듯한 React 코드가 3초 만에 나옵니다. 2026년에 이건 더 이상 놀랍지도 않습니다.
그런데 우리가 풀어야 했던 문제는 그 옆에서 한 칸 더 들어간 곳에 있었습니다. 우리가 만든 건 자연어 설명이나 디자인 시안을 받아서 정해진 디자인 시스템의 컴포넌트로 화면을 그려내는 서비스입니다. 여기서 진짜 요구사항은 “코드가 나온다”가 아니라 이것이었습니다.
우리가 정의한 컴포넌트로만, 디자인 시스템을 따라서.
이 한 줄 차이가 프로젝트의 8할이었습니다. 이 글은 그 간극을 메우면서 부딪힌 것들, 그리고 한 번 크게 데인 경험에 대한 이야기입니다. 풀어야 할 게 많아서 아마 여러 편이 될 것 같고, 이건 그 첫 번째입니다.
1. “그럴듯한 코드”와 “우리 코드”는 다른 종목입니다
범용 코드 생성과 우리 문제는 세 군데에서 완전히 갈렸습니다.
① 임의 컴포넌트 금지
모델이 신나서 <div>에 인라인 스타일 발라가며 UI를 조립하면? 망합니다. 반드시 디자인 시스템에 정의된 컴포넌트(버튼, 그리드, 다이얼로그…)와 허용된 props만 써야 합니다. 사용해야 하는 어휘가 정해져 있는 글쓰기인 셈입니다.
② 시각 품질 보장
동작은 하는데 못생긴 코드는 탈락입니다. 레이아웃, 다단 헤더, 정렬, 간격이 디자인 가이드대로 떨어져야 합니다. 기준은 딱 하나, 사람이 봤을 때 “어, 시안 그대로네”입니다.
③ 결정성
같은 요청이면 비슷하게 나와야 하고, 한 번 만든 화면을 고칠 때 건드리지도 않은 데가 바뀌면 안 됩니다. LLM한테 일관성을 요구하는 것 자체가 도전입니다.
LLM은 ①②③ 없이 그럴듯한 코드는 기가 막히게 뽑습니다. 문제는 우리가 원한 게 그럴듯한 코드가 아니라 우리 코드였다는 점입니다. 그리고 그 둘 사이의 거리는, 해보기 전엔 몰랐을 만큼 멀었습니다.
2. 그래서 프롬프트를 ’컴파일러’처럼 짰습니다
우리가 잡은 멘탈 모델은 이것이었습니다. 디자인 시스템은 문법이고, 프롬프트는 그 언어 명세입니다. 모델이 문법을 못 어기게 만드는 게 일이었습니다.
컴포넌트 스키마를 프롬프트로 주입. 디자인 시스템의 컴포넌트 정의 (이름, props, 사용 규칙) 를 구조화해두고 생성 시점에 시스템 프롬프트로 밀어넣었습니다. 모델이 쓸 수 있는 단어장을 명세로 못박은 셈입니다. 단어장에 없는 단어를 쓰면? 안 되는 겁니다.


시각 품질은 결국 ’룰북’이 됐습니다. 여기가 진짜 손이 많이 가는 작업이었습니다. 모델은 멀쩡하다가도 데이터 표에서 다단 헤더/바디를 만들 때면 꼭 사고를 쳤습니다. 자식이 하나뿐인데 굳이 그룹으로 한 번 더 감싸거나, 헤더 깊이를 쓸데없이 3단 4단으로 늘리거나, 합계 행과 상세 헤더가 충돌하는 구조를 뱉거나.
예를 들면 이런 식입니다.
# 모델이 자주 내던 것 (불필요한 깊이)
[기본정보]
└ [연락처]
└ 휴대폰 ← 자식 하나뿐인데 그룹을 두 번 감쌌다
# 우리가 원한 것 (평탄화)
[기본정보]
└ 휴대폰
이런 패턴을 하나씩 안티패턴 룰로 명문화했습니다. “자식이 하나뿐인 그룹 헤더는 만들지 마라”, “이런 충돌이 생기면 이렇게 변환해라” — 이런 규칙이 쌓이고 쌓여서, 우리 프롬프트는 어느새 한 줄짜리 지시문이 아니라 수십 개의 시각 품질 규칙이 들어찬 룰북이 됐습니다. 컴파일러가 소스를 검사하듯, 프롬프트가 모델의 출력을 미리 제약하는 구조입니다.
이 룰북을 어떻게 쌓고, 계속 늘어나는 규칙들이 서로 부딪히지 않게 관리했는지는 그 자체로 한 편입니다. 그 이야기는 다음에 따로 다루도록 하겠습니다.
“Code as Context”
결정성과 수정 일관성을 위해 한 가지 전략을 더 썼습니다. 이전 대화를 줄줄이 끌고 가는 대신, 직전에 생성된 코드 그 자체를 컨텍스트로 넣고 “이 코드에서 요청한 부분만 정확히 고쳐라”를 핵심 규칙으로 박았습니다. 오래된 대화 히스토리가 만드는 컨텍스트 오염을 끊은 겁니다. 입력 명세가 너무 길면? 경량 모델로 먼저 핵심만 압축해서, 본 모델이 분량에 휩쓸려 길을 잃지 않게 했습니다.
여기까지는 솔직히 잘 굴러갔습니다. 꽤 괜찮은 생성기를 만든 줄 알았습니다.
3. 모델이 호텔 시상식 기사를 뱉던 날
그러던 어느 날, 개발 환경에서 테스트를 돌리다 예상 못 한 일을 만났습니다.

한 업무용 목록 화면을 만들어 달라고 요청했는데, 시스템이 내놓은 건 영국 어느 시상식 결과를 정리한 영문 보도자료였습니다. 코드는 한 줄도 없이, 완벽하게 매끄러운 영어 기사 한 편.
입력 어디에도 호텔이니 시상식이니 하는 단어는 없었습니다. 이건 순수하게 모델 머릿속에서 흘러나온, 이른바 recitation, 학습 데이터를 거의 그대로 되뇌어버리는 환각에 가까웠습니다. AI도 가끔은 일에서 손 놓고 딴 데 다녀오고 싶었던 걸까요. 원인을 찾아야 했습니다.

추적은 완전히 명탐정 코난 에피소드와 비슷했습니다.
용의자 1: 전달이 꼬였나?
다른 사람 요청의 응답이 엉뚱한 곳으로 배달된 건 아닐까? 스트리밍 브로드캐스트 쪽이 의심스러웠습니다. → 저장된 데이터를 직접 까봤습니다. 그 기사는 해당 메시지에 그대로 저장돼 있었습니다. 배달 사고가 아니라, 모델이 진짜로 그걸 생성한 것이었습니다. 용의자 1, 무혐의.
용의자 2: 우리 프롬프트가 모델을 헷갈리게 했나?
그날의 입력을 1:1로 복제해서 같은 모델에 8번, 10번 돌렸습니다. → 전부 멀쩡한 코드가 나왔습니다. 결정적 버그가 아니었습니다. 비결정적이고 일시적인 이탈. 항상 느끼는 것이지만 재현이 안 되는 버그만큼 사람 미치게 하는 것도 없습니다.
결정적 단서: 그날 무슨 일이?
로그를 시간순으로 깔았습니다. 그 생성만 유독 8분이 걸렸습니다. 평소엔 수십 초면 끝나는 작업인데 말입니다. 같은 시간대 인프라도 부하 상태였고, 우리가 쓰던 모델은 프리뷰 버전 — 안정화 전 빌드였습니다. 한 번 레일을 벗어난 모델이 8분을 헤매다 엉뚱한 걸 뱉었고, 아무도 그걸 눈치채지 못했습니다.
그리고 가장 뼈아픈 한 줄. 그 응답은 에러 하나 없이 “성공”으로 저장돼 있었습니다. 코드 생성 요청이었는데, 코드가 0줄이었는데도요. 시스템 입장에선 그냥 성공한 작업이었습니다.
한 가지 분명히 해두면 이건 운영 환경이 아니라 개발 환경에서 테스트하던 중 마주친 일입니다. 실제 사용자에게 이 엉뚱한 응답이 나간 적은 없습니다. 하지만 비결정적이고 일시적인 현상이라, 운영에서도 똑같이 일어나지 말란 법이 없었습니다. “아직 운영에선 안 터졌으니 괜찮다”가 아니라, 터지기 전에 막기로 한 이유입니다.
4. 그래서 우리가 내린 결론: “막을 수 없으면, 새는 걸 막는다”
처음엔 본능적으로 “어떻게 하면 모델이 다시는 이런 짓을 못 하게 할까”를 고민했습니다. 모델을 바꿔? 더 안정적인 버전으로? 비용이 3배였습니다. 게다가 비결정적 현상을 결정적으로 막는다는 건 애초에 불가능하다는 걸, 재현 실패가 이미 말해주고 있었습니다.
그래서 질문을 뒤집었습니다.
모델이 이탈하는 건 못 막는다. 그럼 이탈한 결과물이 사용자에게 ’성공’으로 새어 나가는 것은 막을 수 있나?
이건 막을 수 있었습니다. 출력 경로에 안전망을 겹겹이 깔았습니다.
- 출력 검증 게이트. 코드 생성 요청인데 결과에 코드 블록이 0개? 그건 성공이 아닙니다. “완료”로 저장하는 대신 실패로 처리하고 재시도를 안내합니다. 호텔 기사가 “성공한 화면”으로 둔갑하는 길을 끊었습니다.
- 종료 신호 관측. 모델이 비정상 종료(내용 차단·이탈 등)를 알리는 신호를 그제서야 로깅하기 시작했습니다. 다음에 또 새면 왜 샜는지를 추측이 아니라 데이터로 확정하기 위해서입니다. 측정 안 되는 건 못 고칩니다.
- 포맷 fallback. 모델이 약속된 형식 대신 다른 모양으로 코드를 내놓는 경우도 회수하도록 했습니다. 안전망이 멀쩡한 코드까지 “이탈”로 오판하면 그것도 버그니까요.
요지는 하나입니다. 생성 품질에 쏟은 만큼, 검증과 관측에도 투자해야 합니다. AI 기능 만들 때 가장 흔한 함정이 “데모에서 그럴듯하면 됐지”인데, 그 그럴듯함 뒤에서 드물지만 치명적인 이탈이 조용히 새어 나갑니다. 그걸 잡는 건 더 똑똑한 모델이 아니라, 더 꼼꼼한 안전망이었습니다.
🛡️ 우리가 친 방패 총정리
5. 이번에 진짜로 배운 것
- 제약이 곧 제품 가치입니다. “코드를 생성한다”는 이제 흔합니다. 하지만 “정해진 디자인 시스템으로, 시각 품질까지 보장해서 생성한다”는 여전히 어렵고, 그 어려움이 그대로 차별점이 됩니다. 쉬운 건 이미 남들도 합니다.
- 프롬프트 엔지니어링은 컴파일러 설계에 가깝습니다. “잘 좀 해줘”가 아니라, 어휘(컴포넌트)와 문법(룰셋)을 정의하고 위반을 막는 일입니다. 한 줄 프롬프트로 끝나는 마법은 없었습니다.
- AI 기능엔 ’생성’만큼 ’안전망’이 필요합니다. 막을 수 없는 실패가 존재한다는 걸 인정하는 게 출발점입니다. 그다음은, 그 실패가 사용자에게 도달하지 않게 막는 설계입니다.
그래서, 앞으로
우리는 이런 서비스를 만들었고, 아직 한참 갈 길이 멀다고 생각합니다.
지금은 시각 품질을 룰북에 기대고 있지만, 앞으로는 이걸 좀 더 체계적인 검증 — 생성 결과를 자동으로 평가하고 스스로 교정하는 루프 — 로 옮겨가려 합니다. 모델 이탈도 사후에 차단하는 데서 그치지 않고, 사전 신호로 미리 잡아내는 쪽으로 발전시키고 싶습니다.
“디자인 시스템을 그대로 코드로 그려낸다”는 목표를 향해 가면서 가장 크게 얻은 깨달음은 이것입니다 — 잘 그려내는 생성기만큼, 그 생성을 믿을 수 있게 만드는 장치가 중요합니다. AI한테 펜을 쥐여주는 건 쉽습니다. 그 펜이 매번 우리가 원한 그림을 그리게 만드는 게, 진짜 일이었습니다.
다음 이야기에서
이번 한 편에 다 못 담은 이야기가 더 있습니다. 같은 목표를 향한 다른 작업들입니다.
- AI에게 피그마를 ‘읽게’ 만들기. 디자인 파일을 모델이 이해하는 언어로 번역하면서, 무엇을 버리고 무엇을 남길지 끝없이 저울질한 이야기
- 기획자 열 명이 동시에 누르면? 같은 링크를 캐싱하고, 동시성과 인프라 사이에서 균형을 잡은 이야기
- 스트리밍이 자꾸 끊겼다. 토큰을 실시간으로 흘려보내는 일에 숨은 함정, 그리고 덜 보내서 오히려 더 매끄럽게 만든 이야기
그리고 언젠가는, 룰북에 기대지 않고 생성 결과를 스스로 평가하고 고치는 단계까지. 그건 아직 가보지 못한 길이라, 도착하면 또 찾아오겠습니다.