알뜰폰 요금제 모요(MOYO), 모두의 요금제

AI 시대, 결정론적 자동화가 더욱 중요해진 이유

이승로2026.06.09
AI 시대, 결정론적 자동화가 더욱 중요해진 이유

들어가며

안녕하세요. 모요 프론트엔드 개발자 이승로예요.

어딜 가나 AI 에이전트에 대한 이야기가 끊이지 않는 시대가 왔어요. 단순히 새로운 서비스들이 폭발적으로 쏟아질 뿐 아니라 기존의 서비스들을 만들고 유지보수하던 수많은 업무와 프로세스들이 AI를 통해 자동화되고 있어요.

직접 코드를 작성하지 않고 자연어로 모든 것을 해결하는 시대. 마치 과거에 사람이 룰을 하나하나 정해나가던 룰베이스 자동화*는 역사 속으로 사라진 것만 같은 시대예요.

과연 룰베이스 자동화는 구시대의 유물이 된 걸까요?

아니요. 오히려 AI 시대이기에 룰베이스 자동화의 가치가 더욱 부각되고 있어요.

*

룰베이스 자동화는 사람이 미리 정의한 조건과 절차에 따라 동작하는 자동화를 뜻해요. 같은 입력이 들어오면 같은 규칙을 거쳐 같은 결과를 내도록 설계하는 방식이에요.

결정론과 확률론

Figma UI 구현(코드젠)을 자동화한다고 가정해볼게요. A 개발자는 시안을 구성하는 Figma Node를 파싱하여 코드를 생성하는 룰베이스 자동화를, B 개발자는 Figma MCP와 같은 생성형 AI 도구를 사용한 자동화를 구현했어요.

룰베이스 자동화로 구현한 코드젠은 당연히, 동일한 시안에 대해서는 언제든 동일한 코드를 생성해요. 반면 AI 도구를 활용한 코드젠은 같은 시안에 대해 매번 다른 코드를 생성할 수 있어요. 어떤 때는 flex를 활용해 레이아웃을 잡을 수도 있고, 어떨 때는 grid를 사용할 수도 있어요.

A 개발자가 구현을 잘 했다는 가정 하에 룰베이스 자동화로 구현한 코드젠은 항상 시안과 100% 일치하는 결과물을 생성해요. 반면 AI 도구를 활용한 코드젠은 매번 시안과 결과물이 일치하는지 꼼꼼히 확인해주어야 해요.

A 개발자와 B 개발자가 각각 작성한 코드, 과연 누구의 코드를 더 신뢰할 수 있을까요? 아마도 A 개발자의 코드를 더 신뢰할 수 있을 거예요.

만약 A 개발자가 구현을 잘못 했다면 작성한 코드는 제대로 동작하지 않을 거예요. 그럼에도 의미 있는 이유는 같은 시안에 대한 자동화를 몇 번을 돌려도 항상 같은 결과가 나오니, 실패를 재현하고 원인과 해결방안을 명확히 진단할 수 있다는 점에 있어요.

B 개발자의 경우는 달라요. 결과물이 제대로 동작할 수도 있고, 실패할 수도 있어요. AI 모델을 제공하는 주요 회사들이 모두 명시해 두었듯이, "AI는 실수할 수 있기 때문"이에요.

그렇기 때문에 저는 A 개발자의 접근법이 더 신뢰할 수 있다고 생각해요. 불확실성을 피하고자 하는 인간의 본능에 더 부합하는 접근법이기 때문이에요.

이게 바로 결정론(deterministic)확률론(probabilistic)의 차이예요.

불확실성을 피하고자 하는 인간의 본능과 관련된 재밌는 실험이 있어요. Nature Communications에 실린 UCL 연구에서는 참가자들에게 매 라운드마다 돌 A 또는 B 중 하나를 보여주고, 그 돌 아래에 뱀이 있을지 예측하게 했어요. 참가자의 예측이 맞았는지 여부와 관계 없이 돌 아래에서 뱀이 나오면 참가자의 왼손에 전기충격을 가했어요.

총 320 라운드를 진행하면서 각 라운드에서 뱀이 나올 확률은 미리 알려주지 않았어요. 대신 26~38 라운드 주기별로 뱀이 나올 확률을 조정했어요. 예를 들어 1~26 라운드에서는 돌 A 아래에서는 90% 확률, 돌 B 아래에서는 10% 확률로 뱀이 나왔고, 27~64 라운드에서는 돌 A, B 아래에서 각각 50% 확률로 뱀이 나오는 식이었어요. 그리고 4~6 라운드마다 참가자들에게 현재의 주관적 스트레스 점수를 매기도록 했어요.

결과는 흥미로웠어요. 참가자들의 스트레스 지수는 충격을 자주 받은 구간이 아니라, 확률이 50:50에 가까워 결과를 예측할 수 없는 구간에서 가장 높았어요. 참가자들이 직접 매긴 주관적 스트레스 점수는 물론, 동공 크기와 피부 전도도 같은 생리 지표도 모두 같은 패턴을 보였어요. 인간은 위협 자체보다 위협의 예측 불가능성에 더 강하게 반응한다는 걸 보여주는 실험이에요.

결정론과 확률론의 차이

LLM은 본질적으로 확률적 토큰 생성을 기반으로 하기 때문에 비결정론적 특성을 갖고 있어요. ACM에 게재된 최근 연구에 따르면 ChatGPT로 코드 생성 시 동일한 프롬프트로 5번 요청했을 때, 75.76%의 문제에서 단 한 번도 같은 출력을 내지 못했다고 해요. 더 심각한 건 테스트 통과율의 최대 차이가 0%에서 100%까지 벌어지는 경우가 전체 문제의 39.63%에 달한다는 거예요.

반면 룰베이스 자동화는 명확해요. A라는 입력에 대해 반드시 B라는 결과를 내요. 함수형 프로그래밍에서 강조하는 순수 함수와도 비슷해요. 같은 입력에는 항상 같은 출력을 보장하고, 부작용이 없어요.

함수형 프로그래밍 개발자들이 순수 함수를 강조하는 이유가 바로 여기에 있어요. 순수 함수는 테스트하기 쉽고, 예측 가능하며, 디버깅이 간단해요. 버그가 생기면 함수의 입력과 로직만 확인하면 돼요. 룰베이스 자동화도 마찬가지예요. 룰이 올바르다면, 결과도 항상 올바르고, 문제가 생기면 룰만 고치면 돼요.

확률론 기반 자동화에는 눈에 보이지 않는 비용이 따라와요

확률론 기반 자동화는 동작이 보장되지 않아요. 같은 입력에도 다른 출력이 나올 수 있어요. 이는 개발자에게 더 큰 검증 부담을 지우고요, 이건 곧 비용으로 이어져요.

전통적으로 소프트웨어 엔지니어링에서 가장 골치 아프고, 유지보수 비용을 많이 소모하는 부분 중 하나가 있어요. 바로 재현이 안 되는 버그예요.

재현 불가능한 버그는 디버깅을 악몽으로 만들어요. 같은 입력을 넣어도 다른 결과가 나온다면, 문제의 원인을 특정할 수 없어요. 이것이 바로 확률론적 시스템의 본질적인 문제예요.

비결정적 동작이 유지보수 비용을 증가시키는 이유는 바로 검증 비용의 구조에 있어요.

결정론적 동작을 검증할 때에는 그 동작을 구성하는 룰에 대한 검증을 진행해요. 룰이 올바르다면, 결과도 항상 올바를 것이라 신뢰할 수 있기 때문에 각각 동작에 대한 검증 비용을 줄일 수 있어요.

1룰 정의 (1회)
2→ 룰 검증 (1회)
3→ 동작에 대한 신뢰
1룰 정의 (1회)
2→ 룰 검증 (1회)
3→ 동작에 대한 신뢰

반면 확률론적 동작을 검증할 때에는 각 동작의 결과가 서로 다를 수 있기 때문에 각 동작에 대한 검증 비용이 추가로 발생하게 돼요.

1룰 정의 (1회)
2→ 룰 검증 (1회)
3→ 각 동작에 대한 검증 (N회)
1룰 정의 (1회)
2→ 룰 검증 (1회)
3→ 각 동작에 대한 검증 (N회)

이를 기반으로 결정론적 검증 비용과 확률론적 검증 비용을 정량적으로 비교해보면 아래와 같아요. 물론 정확한 실험을 통한 비교는 아니기 때문에, 비유 수준으로 이해해 주세요.

작업결정론적 검증 시간확률론적 검증 시간100회 반복 시
API 타입 생성룰 1회 검증 = 1시간결과 매번 검증 = 10분 × 1001시간 vs 16.7시간
코드 포맷팅룰 1회 설정 = 30분결과 매번 확인 = 2분 × 1000.5시간 vs 3.3시간
인프라 프로비저닝템플릿 1회 작성 = 2시간결과 매번 검증 = 30분 × 1002시간 vs 50시간

모요에서는 OpenAPI 명세를 통해 API 인터페이스와 연동 코드를 자동 생성해 사용하고 있는데요, 개발자들은 생성된 코드를 거의 보지 않아요. 이 코드 생성은 결정론적 동작을 기반으로 자동화되어 있기 때문인데요, 만약 이를 LLM을 활용하는 확률론 기반 자동화로 구현했다면 생성된 코드를 매번 꼼꼼히 검토해야 했을 거예요. 요청과 응답 인터페이스가 실제 API 동작과 일치하는지, 에러 핸들링이 빠져 있진 않은지, 보안 취약점은 없는지요.

물론 룰베이스도 완벽하진 않아요. 룰 자체에 버그가 있거나, 예상하지 못한 엣지케이스가 있을 수 있어요. 하지만 한 번 검증된 룰은 계속 신뢰할 수 있어요. 매번 결과를 의심할 필요가 없어요.

AI는 무급으로 일하지 않는다.

위의 내용들을 모두 차치하더라도 결정론적인 자동화를 더 적극적으로 사용해야 하는 중요한 이유가 하나 더 있어요. 바로 확률론적 자동화를 위한 도구인 AI는 무급으로 일하지 않는다는 점이에요.

특히 AI 도구들은 더 복잡한 문제를 풀고 더 많은 이해관계나 맥락을 고려할수록 금전적인 비용이 증가해요. 반면 스크립트나 코드를 기반으로 구현되는 결정론적 자동화를 구현하고 실행하는데에는 금전적인 비용이 들지 않아요.

실행 한번의 비용은 크지 않을 수 있어도, 실행이 쌓일수록 금전적인 비용 측면에서의 차이는 점점 더 커져요.

창의성과 일관성: 언제 무엇을 선택할 것인가

확률론의 강점: 창의적 탐색

확률론 기반 AI의 가장 큰 강점은 인간이 생각하지 못한 패턴을 제안한다는 거예요.

예를 들어, 복잡한 비즈니스 로직을 구현할 때 AI는:

  • 여러 가지 구현 방식을 제시해요
  • 엣지케이스를 예측해요
  • 기존 패턴과 다른 창의적 접근을 제안해요
  • 코드베이스의 맥락을 고려한 제안을 해요

이런 탐색적 작업에서는 확률론의 불확실성이 오히려 장점이에요. 여러 가능성을 빠르게 시도해보고, 그중 가장 좋은 것을 선택할 수 있으니까요.

프로토타이핑 단계에서 AI는 탁월해요. 빠르게 여러 아이디어를 구현해보고, 어떤 방향이 맞는지 실험할 수 있어요. 이 단계에서는 "충분히 좋은"이 중요하지, "완벽하게 같은"이 중요하지 않아요.

결정론의 강점: 일관된 실행

반면 결정론 기반 룰베이스의 강점은 동일한 작업을 반복적으로 일관되게 수행하는 거예요.

프로덕션 환경에서 필요한 것은:

  • 예측 가능한 동작
  • 재현 가능한 결과
  • 검증 가능한 과정
  • 추적 가능한 변경

이런 실행적 작업에서 창의성은 심각한 위험 요소예요. 배포 파이프라인이 "창의적으로" 다른 방식을 채택해 배포한다면? 코드 포맷터가 "창의적으로" 다른 스타일을 적용한다면? 재앙이죠.

제약이 만드는 설계의 질

앞에서 말한 일관된 실행 외에도, 룰베이스에는 설계 과정 자체의 가치가 있어요. 단순히 "반복 작업을 줄인다"는 것 이상으로, 문제를 깊이 이해하게 되는 과정이거든요.

룰을 설계하면 문제를 이해한다

ESLint 룰을 직접 작성해본 적 있나요? 룰을 만들려면:

  1. 문제 패턴을 정확히 정의해야 해요
  2. 엣지케이스를 모두 고려해야 해요
  3. 예외 상황을 명확히 해야 해요
  4. 다른 룰과의 충돌을 검토해야 해요

이 과정에서 "왜 이 패턴이 문제인가?", "어떤 상황에서 허용되어야 하나?" 같은 근본적인 질문에 답하게 돼요. 단순히 도구를 사용하는 게 아니라, 도메인을 이해하게 되는 거예요.

만약 LLM에게 ESLint 룰을 정의해달라고 요청한다면 당연히 직접 작성하는 것보다는 빠르게 룰을 정의해 줄 거예요. 그러나 그 룰을 설계하는 과정에서 얻는 통찰은 놓치게 될 수도 있어요.

도구보다 오래 가는 역량

이렇게 문제를 구조화하는 경험이 쌓이면, 특정 도구에 묶이지 않는 역량이 돼요.

Dave Farley는 "Modern Software Engineering"를 비롯한 본인의 다양한 아티클과 채널에서 소프트웨어 엔지니어링을 정의했어요:

"지속 가능하게 가치를 전달하는 것"

빠르게 만드는 것도 중요하지만, 지속 가능하게 만드는 것이 더 중요해요. 1년, 3년, 5년 후에도 유지보수할 수 있어야 하고, 팀원이 바뀌어도 이해할 수 있어야 하고, 요구사항이 바뀌어도 확장할 수 있어야 해요.

룰을 설계하는 역량은 도구의 수명과 무관해요. ESLint가 사라져도, Prettier가 대체되어도, 룰베이스로 문제를 구조화하는 사고방식은 여전히 유효해요.

불과 1년 전에는 프롬프트 엔지니어링이 화두였지만, 지금은 하네스 엔지니어링, 거버넌스 등 새로운 키워드가 등장하고 있어요. 6개월 후면 또 다른 키워드가 나올 거예요. 도구 숙련도의 수명은 도구의 교체 주기에 종속돼요.

반면 신뢰할 수 있는 시스템을 설계하고 구축하는 역량, 책임 있는 자동화를 선택하는 판단력의 수명은 소프트웨어 엔지니어링이 존재하는 한 유효해요. 그리고 이러한 역량을 기반으로 설계된 시스템과 서비스 역시 긴 수명주기를 가질 수 있어요.

소프트웨어 아키텍처의 명작 중 하나인 Unix Philosophy를 떠올려봐요:

"Do one thing and do it well."

Unix는 이 작지만 강력한 철학 아래 설계되어 작은 도구들을 조합해서 강력한 시스템을 만들 수 있어요. grep, awk, sed 같은 도구들이 수십 년간 사용되는 이유 아닐까요?

Hybrid 접근법: 장점만 골라내기

최근에는 AI와 룰베이스를 결합한 하이브리드 접근법이 주목받고 있어요. Yang Xu의 Medium 글 "Rule-Based Automation vs AI Agents vs agentic flow"에서 이런 하이브리드 접근법의 장점을 상세히 다뤘어요.

엄격한 시행을 위해서는 규칙을 사용하고 유연성을 위해서는 에이전트를 사용하되, 인수인계는 명시적으로 구축해야 합니다. 가장 탄력적인 아키텍처는 둘 중 하나를 선택하는 것이 아니라, 각 접근 방식의 강점을 가장 적합한 곳에서 활용하여 두 가지 방식을 모두 조율합니다.

Rule Maker Pattern

SDD 도구를 만드는 Tessl의 "The Rule Maker Pattern" 아티클에서도 Hybrid 접근법을 적용한 Rule Maker Pattern을 소개했어요:

  1. AI가 룰이나 명세를 생성
  2. 생성된 룰이 결정론적으로 실행
  3. 필요시 AI가 룰을 개선

이 패턴의 핵심은 AI의 창의성으로 복잡한 패턴을 인식하고 룰을 생성하되, 실제 변환은 룰베이스의 신뢰성을 바탕으로 예측 가능하게 수행한다는 점이에요. 또한 생성된 룰을 직접 검토하고 테스트할 수 있어 검증 가능성을 확보하고, 한 번 생성된 룰을 반복적으로 사용할 수 있어 재사용성까지 얻을 수 있어요.

Continuous AI

GitHub의 "Continuous AI in practice" 보고서에서 Continuous AI라는 개념을 제시했어요. 전통적 CI/CD는 테스트, 빌드, 린트 등 결정론적 작업에 집중하고, Continuous AI는 코드 리뷰, 의미적 이슈 감지 등 판단이 필요한 작업을 담당해요. 두 가지를 병행하되 혼용하지 않는 것이 핵심이에요.

Hybrid 접근법으로 UI 개발의 고통 덜어내기

프론트엔드 개발자에게 UI 개발은 꽤 고통스러운 작업이에요. 디자이너가 만든 시안을 눈으로 확인하며 코드로 옮겨야 하는데요. 단순 반복에 가까운 작업임에도 개발자의 리소스를 많이 소모하고, 그 과정에서 실수가 발생할 가능성도 높아요.

모요에서도 이를 해결하기 위해 많은 개발자들이 Figma MCP와 같은 LLM 기반 도구들을 활용하고 있어요. 하지만 이런 도구들은 대부분 확률론적 동작을 기반으로 하다 보니, 생성된 코드가 실제 시안과 다른 경우가 자주 발생해요. 결국 결과물을 검증하고 수정하는 데 다시 많은 시간이 들어가요.

모요에서는 이 과정을 하이브리드 자동화로 해결하기 위해 고민하고 있어요. 그리고 그 고민을 바탕으로 figma-to-code라는 스킬을 만들어 가고 있어요.

figma-to-code 스킬의 핵심은 동작하는 코드를 생성하는 결정론적 로직과, 생성된 코드를 실제 코드베이스의 맥락에 맞게 더 읽기 쉬운 형태로 리팩터링하는 확률론적 로직을 결합하는 것이에요.

기존 방식으로 피그마 시안을 코드로 생성할 때 가장 아쉬운 점은, Figma MCP나 Figma REST API가 제공하는 메타데이터만으로는 디자인 시안을 충분히 표현하기 어렵다는 점이었어요. 그래서 많은 경우 디자인 시안의 스냅샷을 기반으로 코드가 생성되는데요, 그 결과물의 퀄리티가 좋지 않은 경우가 많았어요.

반면 피그마 웹 환경에서는 window.figma.currentPage.selection 배열을 통해 선택된 노드의 CSS 정보를 포함한 상세 메타데이터를 얻을 수 있어요. CSS 정보까지 활용할 수 있기 때문에, 이를 바탕으로 더 신뢰도 높은 코드를 만들 수 있어요.

figma.currentPage.selection을 통한 노드 정보 조회

이를 활용하기 위해 figma-to-code 스킬은 CDP와 Playwright로 실제 브라우저의 피그마 시안에 직접 접근해요. 그리고 메타데이터 JSON을 추출하는 스크립트를 주입하고, 그 반환값을 받아 신뢰도 높은 메타데이터를 확보해요.

1// 산출되는 figma-node.json
2{
3 "id": "220:99703",
4 "name": "[Overlay] 안녕하신지확인다이얼로그",
5 "type": "NORMAL",
6 "isAsset": false,
7 "hyperlink": null,
8 "text": null,
9 "textStyleId": null,
10 "css": {
11 "display": "flex",
12 "width": "340px",
13 "padding": "10px",
14 "flex-direction": "column",
15 "align-items": "flex-start",
16 "gap": "10px"
17 },
18 "props": null,
19 "variants": null,
20 "componentName": null,
21 "masterComponent": null,
22 "children": [
23 {
24 "id": "220:99692",
25 "name": "안녕하신지확인다이얼로그",
26 "type": "INSTANCE",
27 "isAsset": false,
28 "hyperlink": null,
29 "text": null,
30 "textStyleId": null,
31 "css": {
32 "display": "flex",
33 "padding": "24px 20px 20px 20px",
34 "flex-direction": "column",
35 "align-items": "flex-start",
36 "gap": "24px",
37 "align-self": "stretch",
38 "border-radius": "16px",
39 "background": "var(--Basic-White, #FFF)",
40 "box-shadow": "0 12px 24px 0 rgba(36, 41, 46, 0.16)"
41 },
42 "props": {
43 "Body Text#783:7": {
44 "type": "TEXT",
45 "value": "반갑습니다."
46 },
47 "Title Text#783:0": {
48 "type": "TEXT",
49 "value": "안녕하세요?"
50 },
51 "Size": {
52 "value": "mobile",
53 "type": "VARIANT",
54 "boundVariables": {}
55 },
56 "Button Type": {
57 "value": "double-primary",
58 "type": "VARIANT",
59 "boundVariables": {}
60 },
61 "Image": {
62 "value": "false",
63 "type": "VARIANT",
64 "boundVariables": {}
65 },
66 "Callout": {
67 "value": "false",
68 "type": "VARIANT",
69 "boundVariables": {}
70 }
71 },
72 "variants": {
73 "Size": "mobile",
74 "Button Type": "double-primary",
75 "Image": "false",
76 "Callout": "false"
77 },
78 "componentName": "01 Dialog",
79 "masterComponent": {
80 // 컴포넌트 정의 노드 정보
81 },
82 "children": [
83 // 자식 노드 정보 배열
84 ]
85 }
86 ]
87}
1// 산출되는 figma-node.json
2{
3 "id": "220:99703",
4 "name": "[Overlay] 안녕하신지확인다이얼로그",
5 "type": "NORMAL",
6 "isAsset": false,
7 "hyperlink": null,
8 "text": null,
9 "textStyleId": null,
10 "css": {
11 "display": "flex",
12 "width": "340px",
13 "padding": "10px",
14 "flex-direction": "column",
15 "align-items": "flex-start",
16 "gap": "10px"
17 },
18 "props": null,
19 "variants": null,
20 "componentName": null,
21 "masterComponent": null,
22 "children": [
23 {
24 "id": "220:99692",
25 "name": "안녕하신지확인다이얼로그",
26 "type": "INSTANCE",
27 "isAsset": false,
28 "hyperlink": null,
29 "text": null,
30 "textStyleId": null,
31 "css": {
32 "display": "flex",
33 "padding": "24px 20px 20px 20px",
34 "flex-direction": "column",
35 "align-items": "flex-start",
36 "gap": "24px",
37 "align-self": "stretch",
38 "border-radius": "16px",
39 "background": "var(--Basic-White, #FFF)",
40 "box-shadow": "0 12px 24px 0 rgba(36, 41, 46, 0.16)"
41 },
42 "props": {
43 "Body Text#783:7": {
44 "type": "TEXT",
45 "value": "반갑습니다."
46 },
47 "Title Text#783:0": {
48 "type": "TEXT",
49 "value": "안녕하세요?"
50 },
51 "Size": {
52 "value": "mobile",
53 "type": "VARIANT",
54 "boundVariables": {}
55 },
56 "Button Type": {
57 "value": "double-primary",
58 "type": "VARIANT",
59 "boundVariables": {}
60 },
61 "Image": {
62 "value": "false",
63 "type": "VARIANT",
64 "boundVariables": {}
65 },
66 "Callout": {
67 "value": "false",
68 "type": "VARIANT",
69 "boundVariables": {}
70 }
71 },
72 "variants": {
73 "Size": "mobile",
74 "Button Type": "double-primary",
75 "Image": "false",
76 "Callout": "false"
77 },
78 "componentName": "01 Dialog",
79 "masterComponent": {
80 // 컴포넌트 정의 노드 정보
81 },
82 "children": [
83 // 자식 노드 정보 배열
84 ]
85 }
86 ]
87}

특히 이렇게 얻은 메타데이터에는 단순 CSS 정보뿐만 아니라 시안을 구성하기 위해 사용한 컴포넌트 정보도 함께 기록되어 있어요. 그리고 각 컴포넌트는 메타데이터를 어떤 코드로 변환할지 정의한 레시피 파일을 가지고 있어요. 코드 생성기는 이 레시피를 기준으로 메타데이터를 해석하기 때문에, 화면을 단순한 스타일 덩어리가 아니라 실제 제품에서 사용하는 컴포넌트 조합으로 만들 수 있어요.

1import { z } from 'zod';
2
3import { createRecipe } from '../../recipe';
4
5const DialogControlSchema = z.object({
6 /** Figma "Title Text#783:0" 프로퍼티 — 다이얼로그 제목 */
7 'Title Text#783:0': z.string(),
8 /** Figma "Body Text#783:7" 프로퍼티 — 본문 텍스트 */
9 'Body Text#783:7': z.string(),
10 /** Figma "Size" 프로퍼티 */
11 Size: z.enum(['mobile', 'desktop']).optional(),
12 /** Figma "Button Type" 프로퍼티 */
13 'Button Type': z
14 .enum([
15 'double-primary',
16 'double-negative',
17 'single-primary',
18 'single-mono',
19 'double-vertical-primary',
20 'double-vertical-thirdparty',
21 ])
22 .optional(),
23 /** Figma "Image" 프로퍼티 — 이미지 표시 여부 */
24 Image: z.enum(['false', 'true']).optional(),
25 /** Figma "Callout" 프로퍼티 — Callout 표시 여부 */
26 Callout: z.enum(['false', 'true']).optional(),
27});
28
29/** 각 variant recipe의 공통 createRecipe 설정 */
30const makeDialogRecipe = () =>
31 createRecipe({
32 figmaName: '01 Dialog',
33 componentName: 'Dialog',
34 importFrom: '@repo/design-system',
35 schema: DialogControlSchema,
36 });
37
38// ── single-primary ───────────────────────────────────────────────────────────
39
40export const dialogSinglePrimaryRecipe = makeDialogRecipe()
41 .case('single-primary', () => ({ 'Button Type': 'single-primary' }))
42 .imports(() => ({
43 ds: {
44 from: import('.'),
45 destructure: { useDialog: true },
46 },
47 }))
48 .mapChildren(() => ({
49 rightButtonLabel: [1, 0], // 01 Button 인스턴스 → TEXT (button area 프레임 없음)
50 }))
51 .action(({ imports: { useDialog }, $ }) => {
52 const dialog = useDialog();
53 const openDialog = () => {
54 dialog.open({
55 type: 'single-primary',
56 title: $.props('Title Text#783:0'),
57 bodyText: $.props('Body Text#783:7'),
58 rightButton: { label: $.children('rightButtonLabel') },
59 });
60 };
61 openDialog();
62 })
63 .view((_, { $ }) => <button onClick={$('openDialog')}>다이얼로그 열기</button>)
64 .build();
65
66// ── single-mono ──────────────────────────────────────────────────────────────
67
68export const dialogSingleMonoRecipe = makeDialogRecipe()
69 .case('single-mono', () => ({ 'Button Type': 'single-mono' }))
70 .imports(() => ({
71 ds: {
72 from: import('.'),
73 destructure: { useDialog: true },
74 },
75 }))
76 .mapChildren(() => ({
77 rightButtonLabel: [1, 0], // 01 Button 인스턴스 → TEXT (button area 프레임 없음)
78 }))
79 .action(({ imports: { useDialog }, $ }) => {
80 const dialog = useDialog();
81 const openDialog = () => {
82 dialog.open({
83 type: 'single-mono',
84 title: $.props('Title Text#783:0'),
85 bodyText: $.props('Body Text#783:7'),
86 rightButton: { label: $.children('rightButtonLabel') },
87 });
88 };
89 openDialog();
90 })
91 .view((_, { $ }) => <button onClick={$('openDialog')}>다이얼로그 열기</button>)
92 .build();
93
94// ── double-primary ───────────────────────────────────────────────────────────
95
96export const dialogDoublePrimaryRecipe = makeDialogRecipe()
97 .case('double-primary', () => ({ 'Button Type': 'double-primary' }))
98 .imports(() => ({
99 ds: {
100 from: import('.'),
101 destructure: { useDialog: true },
102 },
103 }))
104 .mapChildren(() => ({
105 leftButtonLabel: [1, 0, 0], // button area → 첫 번째 버튼 → TEXT
106 rightButtonLabel: [1, 1, 0], // button area → 두 번째 버튼 → TEXT
107 }))
108 .action(({ imports: { useDialog }, $ }) => {
109 const dialog = useDialog();
110 const openDialog = () => {
111 dialog.open({
112 type: 'double-primary',
113 title: $.props('Title Text#783:0'),
114 bodyText: $.props('Body Text#783:7'),
115 leftButton: { label: $.children('leftButtonLabel') },
116 rightButton: { label: $.children('rightButtonLabel') },
117 });
118 };
119 openDialog();
120 })
121 .view((_, { $ }) => <button onClick={$('openDialog')}>다이얼로그 열기</button>)
122 .build();
123
124// ...variant별 recipe 정의
1import { z } from 'zod';
2
3import { createRecipe } from '../../recipe';
4
5const DialogControlSchema = z.object({
6 /** Figma "Title Text#783:0" 프로퍼티 — 다이얼로그 제목 */
7 'Title Text#783:0': z.string(),
8 /** Figma "Body Text#783:7" 프로퍼티 — 본문 텍스트 */
9 'Body Text#783:7': z.string(),
10 /** Figma "Size" 프로퍼티 */
11 Size: z.enum(['mobile', 'desktop']).optional(),
12 /** Figma "Button Type" 프로퍼티 */
13 'Button Type': z
14 .enum([
15 'double-primary',
16 'double-negative',
17 'single-primary',
18 'single-mono',
19 'double-vertical-primary',
20 'double-vertical-thirdparty',
21 ])
22 .optional(),
23 /** Figma "Image" 프로퍼티 — 이미지 표시 여부 */
24 Image: z.enum(['false', 'true']).optional(),
25 /** Figma "Callout" 프로퍼티 — Callout 표시 여부 */
26 Callout: z.enum(['false', 'true']).optional(),
27});
28
29/** 각 variant recipe의 공통 createRecipe 설정 */
30const makeDialogRecipe = () =>
31 createRecipe({
32 figmaName: '01 Dialog',
33 componentName: 'Dialog',
34 importFrom: '@repo/design-system',
35 schema: DialogControlSchema,
36 });
37
38// ── single-primary ───────────────────────────────────────────────────────────
39
40export const dialogSinglePrimaryRecipe = makeDialogRecipe()
41 .case('single-primary', () => ({ 'Button Type': 'single-primary' }))
42 .imports(() => ({
43 ds: {
44 from: import('.'),
45 destructure: { useDialog: true },
46 },
47 }))
48 .mapChildren(() => ({
49 rightButtonLabel: [1, 0], // 01 Button 인스턴스 → TEXT (button area 프레임 없음)
50 }))
51 .action(({ imports: { useDialog }, $ }) => {
52 const dialog = useDialog();
53 const openDialog = () => {
54 dialog.open({
55 type: 'single-primary',
56 title: $.props('Title Text#783:0'),
57 bodyText: $.props('Body Text#783:7'),
58 rightButton: { label: $.children('rightButtonLabel') },
59 });
60 };
61 openDialog();
62 })
63 .view((_, { $ }) => <button onClick={$('openDialog')}>다이얼로그 열기</button>)
64 .build();
65
66// ── single-mono ──────────────────────────────────────────────────────────────
67
68export const dialogSingleMonoRecipe = makeDialogRecipe()
69 .case('single-mono', () => ({ 'Button Type': 'single-mono' }))
70 .imports(() => ({
71 ds: {
72 from: import('.'),
73 destructure: { useDialog: true },
74 },
75 }))
76 .mapChildren(() => ({
77 rightButtonLabel: [1, 0], // 01 Button 인스턴스 → TEXT (button area 프레임 없음)
78 }))
79 .action(({ imports: { useDialog }, $ }) => {
80 const dialog = useDialog();
81 const openDialog = () => {
82 dialog.open({
83 type: 'single-mono',
84 title: $.props('Title Text#783:0'),
85 bodyText: $.props('Body Text#783:7'),
86 rightButton: { label: $.children('rightButtonLabel') },
87 });
88 };
89 openDialog();
90 })
91 .view((_, { $ }) => <button onClick={$('openDialog')}>다이얼로그 열기</button>)
92 .build();
93
94// ── double-primary ───────────────────────────────────────────────────────────
95
96export const dialogDoublePrimaryRecipe = makeDialogRecipe()
97 .case('double-primary', () => ({ 'Button Type': 'double-primary' }))
98 .imports(() => ({
99 ds: {
100 from: import('.'),
101 destructure: { useDialog: true },
102 },
103 }))
104 .mapChildren(() => ({
105 leftButtonLabel: [1, 0, 0], // button area → 첫 번째 버튼 → TEXT
106 rightButtonLabel: [1, 1, 0], // button area → 두 번째 버튼 → TEXT
107 }))
108 .action(({ imports: { useDialog }, $ }) => {
109 const dialog = useDialog();
110 const openDialog = () => {
111 dialog.open({
112 type: 'double-primary',
113 title: $.props('Title Text#783:0'),
114 bodyText: $.props('Body Text#783:7'),
115 leftButton: { label: $.children('leftButtonLabel') },
116 rightButton: { label: $.children('rightButtonLabel') },
117 });
118 };
119 openDialog();
120 })
121 .view((_, { $ }) => <button onClick={$('openDialog')}>다이얼로그 열기</button>)
122 .build();
123
124// ...variant별 recipe 정의

최종적으로 코드 생성기는 레이어 트리의 메타데이터를 읽고, 각 컴포넌트에 연결된 레시피를 적용해 실제 동작하는 TSX 파일을 만들어요. 예시는 단순한 케이스지만, 더 복잡한 시안도 같은 방식으로 처리할 수 있어요. 레이어 트리를 순회하면서 필요한 컴포넌트를 찾고, 하위 레이어에는 동일한 과정을 재귀적으로 적용하는 방식으로 코드를 쌓아 나가요.

1import { useDialog } from '@repo/design-system';
2
3export default function 안녕하신지확인다이얼로그() {
4 const dialog = useDialog();
5 const openDialog = () => {
6 dialog.open({
7 type: 'double-primary',
8 title: '안녕하세요?',
9 bodyText: '반갑습니다.',
10 leftButton: { label: '아니요' },
11 rightButton: { label: '네' },
12 });
13 };
14
15 return (
16 <>
17 <button onClick={openDialog}>다이얼로그 열기</button>
18 </>
19 );
20}
1import { useDialog } from '@repo/design-system';
2
3export default function 안녕하신지확인다이얼로그() {
4 const dialog = useDialog();
5 const openDialog = () => {
6 dialog.open({
7 type: 'double-primary',
8 title: '안녕하세요?',
9 bodyText: '반갑습니다.',
10 leftButton: { label: '아니요' },
11 rightButton: { label: '네' },
12 });
13 };
14
15 return (
16 <>
17 <button onClick={openDialog}>다이얼로그 열기</button>
18 </>
19 );
20}

다만 이렇게 기계적으로 생성한 코드는 가독성이 좋지 않고, 실제 코드베이스의 맥락도 충분히 반영되지 않아요. 그래서 생성된 코드를 더 읽기 쉬운 형태로 리팩터링하고, 적절한 기준으로 파일을 분리해 코드베이스에 적용하는 과정은 AI가 담당하도록 지시하고 있어요.

물론 아직 아쉬운 부분은 많아요. 생성된 코드의 품질을 더 높여야 하고, 실제 코드베이스에 자연스럽게 녹여내는 과정도 계속 다듬어야 해요. 다만 코드 생성을 위한 신뢰도 높은 SSOT가 명확해졌기 때문에, 이를 기반으로 더 빠른 속도로 개선해 나갈 수 있을 것으로 기대하고 있어요.

신뢰성과 속도, 무엇이 더 중요한가

결정론적 자동화의 가치는 AI 시대에도, 아니 AI 시대이기에 더욱 중요해요.

생각해보면 당연해요. 공장 생산 라인이 10배 빨라졌는데, 품질 검사 체계는 그대로라면 어떻게 될까요? 불량품이 10배 빠르게 쌓일 거예요. AI가 코드를 빠르게 생성할 수 있다고 해서, 신뢰할 수 없는 결과를 빠르게 만드는 것은 의미가 없어요.

저는 AI를 부정하려는 게 아니에요. AI는 분명 혁명적인 도구예요. 프로토타이핑, 창의적 제안, 복잡한 패턴 인식에서 AI는 탁월해요.

다만, 모든 자동화가 AI로 대체되어야 한다는 믿음에는 동의하지 않아요.

우리에게 필요한 것은 적재적소의 원칙이에요.

구조화되고 반복 가능한 작업에는 룰베이스를, 창의성과 판단이 필요한 작업에는 AI를. 그리고 가능하다면 둘을 결합하여 AI가 룰을 만들고, 룰에 기반한 정확한 동작을 실행하는 하이브리드 방식을요.

소프트웨어 엔지니어링의 본질은 "빠른 생성"이 아니라 "신뢰할 수 있는 시스템 구축"이에요. 이 본질은 도구가 아무리 발전해도 변하지 않아요.

룰베이스 자동화는 지루할 수 있어요. 룰을 정의하고, 테스트하고, 문서화하는 과정이 AI에게 프롬프트 던지는 것보다 느릴 수 있어요.

하지만 6개월 후, 1년 후, 3년 후를 생각하면 답은 달라져요. 명세 기반으로 생성한 코드는 여전히 신뢰할 수 있지만, AI가 "빠르게" 만든 코드는 기술 부채로 쌓여 있을 가능성이 높아요.

나무는 자라는 소리를 내지 않아요. 하지만 잘 자란 나무는 10년이고 100년이고 사람들이 쉴 수 있는 그늘을 만들어 줘요. 좋은 엔지니어링은 어쩌면 나무와 닮아 있지 않을까요?