기술 포스트

AI를 안 쓰기로 했다


Article

DevTI의 카드 상세에는 "개인 맞춤 강점 분석"이 뜬다. 설문 응답(직군·스킬·관심사·목표·MBTI)을 넣으면 당신만의 칭호와 강점 세 가지, 숨은 무기, 성장 서사가 나온다.

지금 같으면 LLM API를 부르면 된다. 프롬프트 하나로 끝난다.

한 번도 안 불렀다. 문장 조각 124개와 가중치 규칙만으로 만들었다.

타협이 아니었다. 여러 축에서 그쪽이 나았다.


AI를 부르면 잃는 것들

LLM 호출 vs 규칙 엔진

LLM API 호출

프롬프트 하나면 끝

  • 매 호출마다 결과가 다르다
  • 환각(hallucination) 가능
  • 호출당 과금
  • 수 초 걸린다
  • 설문 내용이 외부 서버로 나간다
  • 왜 이 결과인지 설명할 수 없다

규칙 엔진

조각 124개 + 가중치

  • 같은 입력이면 언제나 같은 출력
  • 검증된 문장만 나간다 — 부적절 문구 0
  • 0원 · 무한 확장
  • 밀리초
  • 데이터가 밖으로 안 나간다
  • 왜 이 결과인지 추적 가능하다
"AI를 못 써서"가 아니다. 이 서비스의 성질에는 규칙 쪽이 맞았다.

특히 두 가지가 결정적이었다.

첫째, 결과가 매번 바뀌면 안 된다. DevTI의 카드는 자랑하고 공유하라고 만든 것이다. 링크를 열 때마다 내 칭호가 바뀌면 그건 내 카드가 아니다.

둘째, 사용자가 프로필을 반복해서 고친다. 설문을 여러 번 다시 하며 결과를 보는 구조다. LLM이라면 그때마다 과금되고, 어뷰징 방지도 따로 짜야 한다. 규칙 엔진은 몇 번을 다시 계산해도 0원이다.

그래서 분석 결과를 데이터베이스에 저장하지도 않는다. 같은 입력이면 같은 출력이니, 읽을 때마다 다시 계산하면 그만이다. 저장 안 하니 정합성 문제도 없다.


다양성은 조합에서 나온다

문장을 몇 개나 써야 하나. 이게 규칙 엔진의 진짜 비용이다.

MBTI 16타입에 글 하나씩 쓰면 16개다. 그러면 같은 타입인 사람은 전부 같은 글을 본다. 뻔하다.

그래서 MBTI를 16타입이 아니라 4축으로 쪼갰다. E/I · N/S · T/F · J/P를 각각 0과 1로 두고, 그 축을 직군·관심사·목표와 교차시킨다.

조각은 다섯 개의 슬롯으로 나뉜다. 화면의 각 섹션이 슬롯 하나다.

슬롯 무엇을 채우나 조각 수
headline 커스텀 칭호 31
strength 핵심 강점 40
hidden_weapon 숨은 무기 (흔치 않은 조합을 짚는다) 17
growth 성장 서사 (현재 → 목표) 27
catchphrase 한 줄 캐치 9

합계 124개.


콘텐츠를 코드에서 떼어냈다

핵심 설계는 하나다. 규칙과 콘텐츠를 코드가 아니라 데이터로 둔다.

조각 하나는 이렇게 생겼다.

{
  "id": "hl-backend-architect",
  "slot": "headline",
  "conditions": [
    { "feature": "jobRole", "op": "eq",  "value": "BACKEND" },
    { "feature": "mbti.T",  "op": "gte", "value": 1 }
  ],
  "weight": 10,
  "rarity": 3,
  "template": "구조를 빚어내는 {mbtiAdj}형 백엔드 설계자"
}

conditionsAND다. 전부 만족해야 후보가 된다. 백엔드 직군이면서 T축인 사람에게만 이 칭호가 걸린다.

weight는 후보끼리의 우선순위, rarity는 희소도 기여, template{mbtiAdj}는 나중에 채워진다.

이게 fragments.json 하나에 들어 있다. 엔진 코드를 한 줄도 안 건드리고 문구를 추가·수정할 수 있다. 문구를 고쳐도 재배포가 필요 없다 — 콘텐츠가 코드가 아니기 때문이다.


파이프라인 일곱 단계

각 단계는 단일 책임을 가진 순수 함수다. 같은 입력이면 같은 출력이라 단위 테스트가 쉽다.

설문 하나가 분석이 되기까지
  1. 벡터화ProfileVectorizer — 설문을 정규화한다. MBTI 는 여기서 4축으로 쪼개진다
  2. 특성 점수TraitScorer — 능력치 5축을 기여도 행렬로 계산
  3. 희소성RarityScorer — "상위 7%의 조합" 같은 백분위
  4. 규칙 매칭RuleMatcher — 슬롯별로 조건을 전부 만족하는 조각만 추린다
  5. 결정적 선택FragmentSelector — 랜덤이 아니라 프로필 해시로 고른다
  6. 합성NarrativeComposertemplate 에 변수를 채워 문장으로
  7. 렌더칭호 · 강점 · 숨은 무기 · 성장 서사 · 레이더 차트
일곱 개 다 순수 함수다. 상태가 없으니 어느 단계든 따로 떼어 테스트할 수 있다.

능력치 계산은 이렇게 생겼다. 마법이 아니라 덧셈이다.

designScore = 50 (기본)
            + (skill: Figma          → +15)
            + (interest: 디자인시스템  → +10)
            + (mbti.N                → +5)
            → clamp(0, 100)

왜 이 점수가 나왔는지 한 줄씩 짚을 수 있다. LLM이었다면 "그렇게 나왔다"밖에 못 한다.


랜덤이 아니라 해시다

후보가 여럿일 때 하나를 고르는 자리 — 여기가 이 설계의 심장이다.

랜덤으로 고르면 새로고침할 때마다 칭호가 바뀐다. 그건 내 카드가 아니다.

그래서 프로필 해시로 고른다.

int idx = stableHash(seed) % topTier(ranked).size();

같은 프로필이면 같은 해시가 나오고, 같은 인덱스가 나오고, 같은 조각이 뽑힌다. 사람마다 다르지만 같은 사람은 언제나 같다.

여기에 한 겹을 더 뒀다. weight 동률 그룹 안에서만 섞는다.

// weight 동률 그룹 단위로 해시 회전 → 높은 weight 우선, 동률 안에서만 섞는다.

가중치가 높은 조각이 무조건 먼저다. 그 안에서 여럿이 비기면, 그때 해시로 고른다. 우선순위는 지키면서 다양성을 얻는다.


정직하게 — 이 방식의 한계

규칙 엔진은 공짜가 아니다. 다양성이 콘텐츠 큐레이션에 묶인다.

조각을 안 쓰면 결과도 안 늘어난다. LLM은 문장을 무한히 만들어내지만, 우리는 124개가 전부다. 그 이상의 표현은 사람이 더 써야 나온다.

실제로 그 한계를 밟았다. 처음엔 헤드라인이 직군당 두 개뿐이었다. MBTI가 달라도 칭호가 거의 똑같이 보였다. 조각 풀이 얇으면 조합의 힘이 안 나온다.

그래서 헤드라인을 직군 × 기질(NT/NF/SJ/SP) 16종으로 다시 짜고, 형용사도 기질 × E/I로 8종을 뒀다. 칭호 어간 16 × 형용사 8 = 최대 128 조합이다.

확인은 눈이 아니라 테스트로 했다. 8 MBTI × 4 직군 = 32 프로필을 돌려 서로 다른 헤드라인이 몇 개 나오는지 셌다. 28개. 이전엔 사실상 직군당 2개였다.

규칙 엔진의 비용은 여기서 발생한다
  1. 01

    조각이 얇으면 조합이 안 산다

    헤드라인이 직군당 2개뿐이던 시절, MBTI 가 달라도 칭호가 거의 같았다. 조합의 힘은 풀의 두께에서 나온다.

  2. 02

    축을 늘려 조각을 교차시킨다

    직군 × 기질 16종 + 기질 × E/I 형용사 8종 → 어간 16 × 형용사 8 = 최대 128 조합.

  3. 03

    눈으로 보지 말고 세어본다

    32개 프로필을 돌려 서로 다른 헤드라인 28개를 확인했다. "다양해 보인다"가 아니라 숫자로.

LLM 이었다면 이 작업이 없다. 대신 결과가 매번 바뀌고, 돈이 들고, 왜 그런지 설명 못 한다.

배운 것

"AI를 안 쓴다"가 뒤처진 선택처럼 들리던 때가 있었다. 실제로는 서비스의 성질이 도구를 정한다.

DevTI의 결과물은 자랑하고 공유하는 것이다. 그러면 결과가 안 바뀌어야 한다. 그리고 사용자가 설문을 반복해서 고친다. 그러면 계산이 공짜여야 한다. 그 둘이 정해지자 답은 규칙 엔진 하나뿐이었다.

이 시리즈에서 계속 나온 패턴이 여기서도 나온다. 1편에서 서버액션을 안 쓴 것도 "못 써서"가 아니라 인증 구조가 그렇게 정해줬기 때문이었다. 도구를 먼저 고르고 구조를 맞추는 게 아니라, 구조가 도구를 고른다.

그리고 하나 더. 규칙 엔진의 진짜 값은 설명할 수 있다는 것이다.

designScore = 50 + Figma 15 + 디자인시스템 10 + N축 5.

누가 "왜 제 디자인 점수가 80이죠?"라고 물으면 답할 수 있다. 그게 이 서비스가 사람에게 주는 신뢰의 정체였다.


다음 편에서는 라이트하우스 이야기를 쓴다. 도구가 시킨 대로 고친 것들과, 알면서도 안 고친 것 하나. 그리고 aria-hidden으로 숨겼는데 접근성 경고가 사라지지 않던 이유.