Code › tail-villain

개발자 도구에서 인터뷰 플랫폼으로

개발자 전용 가정을 걷어내고 여러 직군의 인터뷰를 다루도록 제품 구조를 바꾼 과정

IELTS Speaking 세션에서 Kovill이 고향 발전의 trade-off를 물었다.

질문 자체가 완전히 말이 안 되는 것은 아니었다. 어떤 주제든 장단점을 물을 수는 있지만, 그 문장은 tail-villain이 아직 개발자 면접 도구의 언어를 벗어나지 못했다는 증거였다. 사용자는 영어 말하기 시험을 준비하고 있는데, 인터뷰어는 소프트웨어 설계 면접에서 쓰던 사고방식을 들고 들어오고 있었다.

처음에는 이미 큰 이름을 바꿨다고 생각했다. JobDescription은 Goal이 되었고, Resume은 Background가 되었으며, 데이터베이스 테이블과 API, DTO도 따라 바뀌었다. 그런데 제품의 실제 말투는 스키마 이름보다 더 깊은 곳에 남아 있었다.


범용 플랫폼으로 바꾸는 일은 단순한 라벨 교체가 아니었다.

초기 구조는 내가 가장 잘 아는 개발자 면접에서 출발했다. JD와 이력서를 넣고, 부족한 주제를 뽑고, 기술 질문으로 압박하는 흐름이었다. 이 구조는 소프트웨어 엔지니어에게는 자연스럽지만, IELTS Speaking이나 발표 준비, 자격증 구술 대비 같은 목표에는 너무 좁다.

그래서 핵심 엔티티를 Goal과 Background로 바꿨다. 사용자가 준비하는 목표와 그 사람의 배경을 받아서 어떤 도메인이든 연습 로드맵을 만들 수 있게 하려는 변경이었고, 기존 데이터를 보존하기 위해 테이블은 rename 방식으로 옮겼다. Roadmap, InterviewSession, StudySession의 관계도 goalId와 backgroundId 기준으로 다시 맞췄다.

여기까지는 눈에 보이는 리팩터링이었다. 하지만 실제 제품 경험은 여기서 끝나지 않았다. 프론트의 문구, i18n 키, 테스트 fixture, persona 정의, 프롬프트 안의 예시 문장까지 예전 가정이 흩어져 있었다.

특히 AI 프롬프트 안의 낡은 말들이 문제였다. technical interviewer, technical judgment, trade-offs, JD alignment 같은 표현은 코드 검색으로 잡히기도 하지만, 어떤 표현은 유틸 함수나 세션 종류 설명 안에 숨어 있었다. 화면에는 Goal이라고 보여도, LLM에게 전달되는 설명이 여전히 소프트웨어 면접이면 결과는 그대로 새어 나왔다.


스크린샷이 문제를 한 층씩 드러냈다.

처음에는 Kovill의 소개 문장에 background and technical judgment가 섞여 있었는데, 이건 focus area 설명에서 온 문구였다. 그 다음에는 full mock interview 같은 표현이 IELTS 세션에 들어갔고, 세션 종류 설명을 고쳐야 했다. 또 다른 화면에서는 INTEGRATED 배지가 단일 주제 IELTS 세션에 붙어 있었고, roadmap 세션에만 보여야 할 배지가 범위를 넘어서고 있었다.

첫 번째 단계 라벨도 Career pitch and role-fit narrative였고, 하드코딩된 INTERVIEW_ANGLE_MAP에서 나온 값이었다. 마지막으로 남은 trade-off 질문의 원인은 opening directive의 한 줄, ask for one judgment or trade-off only였다.

이 한 줄이 고향 이야기에 trade-off를 끌고 왔다.

프롬프트에서 어떤 단어를 제거할 때는 하지 말라고 쓰는 것만으로는 부족하다. Do not ask trade-offs라고 쓰면 모델은 여전히 trade-off라는 개념을 보기 때문에, 아예 자연스러운 질문을 하라는 긍정 지시인 short, direct, match the natural framing으로 바꿨다. 후속 질문도 trade-offs, evidence, execution detail 대신 clarity, concrete evidence, domain-relevant detail로 바꿨다.

이 작업을 하면서 persona 이름도 언어별로 정리했다. 영어 세션에 한국어 별칭이 섞이면 persona의 언어 일관성이 깨지기 때문에, 프롬프트에는 응답 언어에 맞는 displayName 또는 localizedName 하나만 넣도록 했다.


로드맵 인터뷰의 평가 기준도 같은 방식으로 바꿨다.

5월 13일에는 roadmapInterviewAngles를 분석 시점에 생성해 analysisSummary에 저장하도록 했다. 도메인에 따라 인터뷰 단계가 달라져야 했고, IELTS와 백엔드 시스템 설계 면접이 같은 angle을 쓰면 안 되기 때문이다.

다음 날에는 evaluationCriteria도 같은 구조로 옮겼다. 이전에는 Role fit, JD alignment 같은 하드코딩된 fallback이 있었고, 중립적인 문장으로 바꾸는 방법도 있었지만 그건 여전히 fallback을 유지하는 방식이었다. 사용자의 목표를 분석해서 기준을 만들 수 있다면, 고정 기준은 제품 안에 남겨둘 이유가 적었다.

그래서 roadmapEvaluationCriteria를 분석 응답 스키마에 추가했고, buildRoadmapInterviewModule은 동적 기준이 있을 때만 그것을 사용하도록 했다. 없으면 빈 배열을 넘기며, 프론트와 프롬프트 양쪽이 빈 값을 문제없이 처리하는지 먼저 확인한 뒤 fallback을 지웠다.

이 결정의 이유는 간단했다. 범용 플랫폼에서는 빈 상태가 잘못된 상태보다 낫고, 잘못된 기준으로 평가하는 것보다 기준이 아직 없음을 명확히 다루는 편이 안전하다.


UI도 같은 방향으로 정리했다.

locked checkpoint는 자물쇠와 대기 중만 보여주지 않고, 실제 질문 제목을 흐리게 보여주도록 바꿨다. 사용자는 앞으로 어떤 흐름이 올지 알아야 한다. START INTERVIEW 배지는 대화가 시작되면 숨겼고, focus area 배지는 roadmap 세션에만 보이도록 범위를 좁혔다. 완료된 세션에서는 채팅 입력창이 남아 있으면 안 되므로 renderInput 자체를 조건부로 빼고, wrapper div도 renderInput이 있을 때만 그리게 했다.

타이머도 정리했다. 완료된 세션에서 카운트다운 0:00과 duration이 동시에 보이는 것은 상태 경계가 애매하다는 신호였고, active 세션일 때만 카운트다운을 보여주고 완료 후에는 소요 시간을 별도 배지로 보여주도록 나눴다. formatDuration이 한국어 분, 초를 직접 반환하던 문제도 고쳤는데, 유틸 함수의 문자열도 i18n 규칙 밖에 있지 않기 때문이다.

URL import 기능은 Goal과 Background 입력을 조금 더 현실적으로 만들었다. 사용자가 채용 공고나 배경 자료 URL을 붙이면, 서버가 공개 ATS API나 HTML 본문 추출을 통해 텍스트를 가져온다. 여기에는 LLM을 쓰지 않았고, URL 하나를 미리보기 위해 모델 호출을 태우는 것은 과했다. SSRF 방어, LinkedIn 차단, 본문 길이 제한 같은 일반적인 HTTP 경계만 먼저 세웠다.


이 작업에서 가장 오래 남은 생각은 용어 변경이 인프라 작업이라는 점이었다.

화면의 JD를 Goal로 바꾸는 것은 시작일 뿐이다. 데이터베이스, DTO, 공유 스키마, i18n, 테스트, 프롬프트, 페르소나 설명, 관리자 화면, 세션 배지, 평가 기준까지 같은 방향을 봐야 하고, 하나라도 예전 세계관을 들고 있으면 LLM은 그 단어를 따라간다.

tail-villain을 범용 인터뷰 플랫폼으로 바꾸는 일은 개발자 면접 기능을 버리는 일이 아니었다. 오히려 소프트웨어 면접도 하나의 목표로 다루기 위해, 제품의 중심을 Goal과 Background로 옮긴 것이다.

도메인을 일반화한다는 것은 문구를 넓게 쓰는 일이 아니다.

프롬프트가 어떤 질문을 떠올리게 하는지, 평가 기준이 어디서 만들어지는지, UI가 어떤 세션에 어떤 정보를 보여주는지까지 모두 다시 정렬하는 일이다. IELTS 세션에서 trade-off가 튀어나온 것은 작은 버그였지만, 그 덕분에 숨어 있던 전제를 끝까지 추적할 수 있었다.