이력서를 실제 코딩 능력을 증명하는 자료로 바꿔주는 AI 플랫폼, One
요약
One은 이력서를 분석하여 실제 기술 스택 기반의 맞춤형 작업 증명(Proof-of-Work) 평가와 개인화된 커리어 로드맵을 제공하는 AI 플랫폼입니다. OpenAI GPT-4o와 Realtime API를 활용해 실시간 음성 멘토링과 정교한 기술 검증 기능을 제공합니다.
핵심 포인트
- 이력서 PDF 기반의 맞춤형 코딩 및 시스템 설계 평가 생성
- OpenAI Realtime API를 활용한 초저지연 음성 AI 멘토링
- 대화 내용을 바탕으로 한 개인화된 6개월 학습 로드맵 구축
- Next.js, Supabase, WebRTC 등 최신 기술 스택 활용
모든 개발자가 겪어봤을 경험입니다. 이력서를 다듬는 데 3시간을 보내고, 자신이 한 번이라도 튜토리얼로 열어본 모든 프레임워크를 나열합니다. 그러고 나서 채용 담당자는 그것을 스캔하는 데 6초밖에 쓰지 않습니다. 면접에서는 실제 업무에서 사용하지 않을 LeetCode 알고리즘을 테스트합니다. 아무도 실제로 당신이 무언가를 구축할 수 있는지 검증해주지 않습니다.
저는 그 상황에 지쳤습니다. 그래서 저는 One이라는 AI 플랫폼을 만들었습니다. 이 플랫폼은 이력서를 분석하고, 실제 스택 기반의 맞춤형 작업 증명(Proof-of-Work) 평가를 생성하며, 실시간 음성 멘토를 제공하고, 대화 내용을 바탕으로 6개월 경력 로드맵을 구축합니다. 모든 것이 하나의 워크스페이스에서 이루어집니다.
제가 만든 것
One은 풀스택 AI 개발자 커리어 플랫폼입니다. 핵심 루프는 다음과 같습니다:
- 이력서 PDF 업로드 → GPT-4o가 당신의 실제 기술(회사의 기술이 아닌, 당신의 기술)을 추출합니다.
- 작업 증명 평가 수행 → 객관식(MCQ), 코딩, 시스템 설계 및 워크플로우에 걸친 27개의 자동 생성 질문을 받습니다.
- Keri와 대화 → OpenAI Realtime API를 기반으로 하며 당신의 점수와 스택을 아는 음성 AI 멘토입니다.
- 6개월 로드맵 받기 → 템플릿이 아닌, Keri와의 실제 대화를 바탕으로 생성됩니다.
기술 스택:
- Next.js 16 (App Router, Turbopack)
- 인증을 위한 Supabase SSR
- 이력서 파싱, 채팅 및 로드맵 생성을 위한 OpenAI GPT-4o
- 음성 처리를 위한 WebRTC 기반 OpenAI Realtime API (
gpt-4o-realtime-preview) - Tailwind CSS v4 + shadcn/ui + Framer Motion
- Vercel에 배포됨
실제 기능 설명
- 이력서 PDF를 파싱(Parsing)하여 지원자가 실제로 사용한 기술만 추출하며, 고용주가 기술 설명에 나열한 기술은 무시함
- 정확히 27개의 질문 생성: 지원자의 실제 프로젝트와 연계된 객관식 문제(MCQ) 20개 + 코딩 챌린지(Coding challenges) 3개 + 시스템 디자인(System design) 2개 + 워크플로우(Workflow) 질문 2개
- 각 기술 영역을 점수화하고 시각적인 기술 그래프(Skill graph)를 렌더링함
- Keri와 실시간 음성 세션을 시작함 (오디오 라운드트립(Audio round-tripping) 없음, 1초 미만의 응답 속도)
- 대화가 끝나면 월별 예상 소요 시간과 활성 월 표시가 포함된 구조화된 6개월 학습 로드맵(Learning roadmap)을 생성함
- 사용자별 데이터 격리가 적용된 완전한 인증 게이트(Auth-gated) 적용 (계정 간 상태 공유 없음)
- 이메일 확인 절차의 번거로움 없이 이메일 가입, Google OAuth, GitHub OAuth를 지원함
주요 기능 (Key Features)
- 이력서 인지형 질문 생성 (Resume-aware question generation): 모든 질문이 지원자의 특정 프로젝트와 기술 스택을 참조함
- 전체 문맥을 파악하는 음성 멘토 (Voice mentor with full context): Keri는 말을 하기 전에 사용자의 이력서 기술, 테스트 점수, 로드맵을 읽음
- REST가 아닌 WebRTC 음성 (WebRTC voice, not REST): 오디오가 브라우저에서 OpenAI로 직접 전달되며, 서버는 오디오 경로에 관여하지 않음
- 대화로부터 생성되는 로드맵 (Roadmap from conversation): Keri와 대화한 후, 버튼 하나로 개인화된 계획을 생성함
- 이메일 확인 불필요 (Zero email confirmation): 서버 측 관리자 API가
email_confirm: true상태로 사용자를 생성함 - 사용자별 localStorage 격리 (Per-user localStorage isolation): 계정을 전환하면 이전 사용자의 데이터가 자동으로 삭제됨
- 모든 요청에 대한 경로 보호 (Route protection on every request):
proxy.ts가 페이지가 렌더링되기 전에 Supabase 세션을 검증함 - 이력서 아님 감지 (Non-resume detection): 호스텔 신청서나 송장을 업로드할 경우, One은 기술을 지어내는 대신 이를 알려줌
제작 과정 (How I Built It)
이력서 파싱: GPT-4o가 거짓말을 멈추게 하는 법
"여기 PDF가 있으니 기술이 무엇인지 알려줘"와 같은 단순한 접근 방식은 작동하지 않습니다. GPT-4o는 지원자가 근무했던 모든 채용 공고에 언급된 모든 기술을 아주 기쁘게 추출해 버릴 것입니다. 그것은 지원자의 기술 스택이 아니라, 고용주의 기술 스택입니다.
저는 OpenAI의 Files API를 사용하여 PDF를 업로드한 다음, 2단계 프롬프트(two-step prompt)를 통해 gpt-4o를 호출했습니다.
const PROMPT = `You are analyzing a document to determine if it is a DEVELOPER/ENGINEER
technical resume and generate a personalized coding assessment.
...
1단계는 게이트(gate) 역할을 합니다. 만약 문서에 개인적인 업무 경험(personal work experience)과 3개 이상의 개인 기술(personal skills)이 포함되어 있지 않다면, 프로세스는 즉시 중단(short-circuits)되며 질문을 생성하지 않습니다. 2단계에는 후보자가 개인적으로 사용한 기술만을 추출하라는 명시적인 지침이 포함되어 있습니다.
그 다음, 모델 자체의 판단 위에 서버 측 가드(server-side guard)를 추가했습니다.
// 모델이 플래그를 표시했거나, 기술이 너무 적거나, 생성된 질문이 없는 경우 이력서가 아닌 것으로 처리
if (parsed.not_resume || skills.length < 3 || questions.length < 5) {
return NextResponse.json({ not_resume: true, skills: [], title: "", questions: [] });
...
모델은 거짓말을 할 수 있습니다. 가드가 이를 잡아냅니다.
한 가지 더 주의할 점이 있습니다. OpenAI의 response_format: { type: "json_object" }는 메시지 어디에도 "json"이라는 단어가 나타나지 않으면 400 에러를 발생시킵니다. 이 사실을 알아내는 데 인정하고 싶지 않을 정도로 긴 시간이 걸렸습니다.
Voice AI: 정신을 잃지 않고 Next.js에서 WebRTC 구현하기
Keri는 1초 미만의 음성 응답을 위해 OpenAI의 Realtime API를 사용합니다. 여기서는 아키텍처가 중요합니다. 서버가 오디오 경로(audio path)에 포함되도록 해서는 안 됩니다. 그렇게 하면 지연 시간(latency)과 비용이 추가됩니다. 올바른 흐름은 다음과 같습니다.
Browser → (SDP offer) → Next.js server → OpenAI /v1/realtime
OpenAI → (SDP answer) → Next.js server → Browser
Browser ←→ OpenAI (직접적인 WebRTC 오디오, 서버는 루프에서 제외)
세션 엔드포인트(session endpoint)는 수명이 짧은 토큰을 생성합니다.
// /api/realtime/session
const res = await fetch("https://api.openai.com/v1/realtime/sessions", {
method: "POST",
...
SDP 엔드포인트는 브라우저의 WebRTC 오퍼(offer)를 OpenAI로 전달하고 응답(answer)을 반환합니다.
// /api/realtime/sdp
const sdpOffer = await req.text();
...
SDP 핸드셰이크 (handshake) 이후, 서버는 오디오 경로에서 완전히 벗어납니다. 브라우저와 OpenAI는 WebRTC를 통해 직접 통신합니다. 중간에 프록시 (proxy)가 없기 때문에 지연 시간 (latency)이 낮은 것입니다.
Keri의 컨텍스트: 당신을 잘 아는 멘토
Keri가 일반적인 챗봇 (chatbot)과 다르게 느껴지는 점은, 모든 대화에 사용자에 대한 실제 데이터가 주입된다는 것입니다:
const ctxLines: string[] = [];
if (context.skills?.length) {
...
GPT-4o로 보내는 모든 메시지에는 사용자의 실제 이력서 기술 (skills), 성적순으로 정렬된 정확한 테스트 점수, 그리고 현재 로드맵 (roadmap) 상태가 포함됩니다. 사용자가 "무엇을 공부해야 할까요?"라고 물었을 때, Keri는 추측하지 않습니다. 사용자의 가장 낮은 점수와 다음 로드맵 달을 확인하여 구체적인 답변을 제공합니다.
인증 (Auth): 보안을 해치지 않고 이메일 확인 단계 건너뛰기
Supabase의 기본 가입 (signup) 흐름은 사용자가 로그인하기 전에 확인 이메일을 보냅니다. 사용자가 즉시 앱에 접속하기를 원하는 평가 도구 입장에서는 최악의 사용자 경험 (UX)입니다.
Supabase는 과거에 이를 위한 UI 토글을 제공했으나, 현재는 제거되었습니다. 해결 방법은 관리자 API (admin API)를 사용하는 것입니다:
// /api/auth/register
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
...
등록 페이지는 이 서버 사이드 (server-side) 엔드포인트를 호출한 다음, signInWithPassword를 통해 즉시 사용자를 로그인시킵니다. 이메일도, 기다림도 필요 없습니다. 바로 접속됩니다.
사용자별 데이터 격리 (Per-User Data Isolation)
One은 이력서 데이터, 테스트 점수, 로드맵 상태를 로컬 스토리지 (localStorage)에 저장합니다. 문제는 동일한 브라우저에서 사용자 B가 로그인한 후 사용자 A가 로그인하면, A가 B의 데이터를 보게 된다는 점입니다.
해결책은 사용자 ID 감시 키 (sentinel key)를 사용하는 것입니다:
const storedUserId = localStorage.getItem("one-current-user");
if (storedUserId !== currentUserId) {
...
또한 이전 사용자의 React 상태 (state)가 남아있지 않도록, 사용자 ID와 연결된 key 프롭 (prop)을 사용하여 대시보드 전체를 다시 마운트 (remount) 합니다:
<main key={userId || "anon"}>
{/* VoiceAgentPage, ChatbotPage, ResumePage 모두 사용자 변경 시 다시 마운트됨 */}
</main>
React key 변경은 전체 언마운트(unmount) → 재마운트(remount) 사이클을 강제합니다. 이를 통해 오래된 상태(stale state)나 계정 간의 데이터 유출(data bleed)을 방지할합니다.
경로 보호 (Route Protection): 대시보드 깜빡임 제로
가장 중요한 UX 요구사항은 인증되지 않은 사용자가 대시보드의 단 한 프레임도 볼 수 없어야 한다는 것입니다. 단 200ms조차 허용되지 않습니다.
Turbopack을 사용하는 Next.js 16은 proxy.ts를 사용합니다 (middleware.ts가 아닙니다. 두 가지를 모두 사용하면 충돌 오류로 빌드가 깨집니다). 모든 요청은 이 파일에 가장 먼저 도달합니다:
const { data: { user } } = await supabase.auth.getUser();
if (!user && !isPublic) {
...
getUser()는 Supabase 서버를 통해 JWT를 검증합니다. 이는 단순히 쿠키를 읽는 것이 아니라 실제 세션 확인(session check)을 수행하는 것입니다. 유효한 세션이 없다면, Next.js가 페이지의 단 1바이트도 렌더링하기 전에 리다이렉트가 발생합니다. 대시보드의 authChecked 상태는 두 번째 레이어 역할을 합니다. 클라이언트 측 세션 확인이 완료될 때까지 컴포넌트는 검은 화면을 렌더링합니다.
대화를 통한 로드맵 생성
Keri와 대화를 나눈 후, 버튼 하나만 누르면 구조화된 6개월 학습 계획이 생성됩니다. 모델은 전체 대화 기록을 전달받아 타입이 지정된 JSON 객체를 생성합니다:
const ROADMAP_PROMPT = `You are analyzing a mentoring conversation to generate a
personalized 6-month learning roadmap with working hours.
...
로드맵은 gpt-4o-mini(response_format: { type: "json_object" }를 사용하여 구조화된 JSON을 생성하기에 충분히 빠르고 저렴함)를 사용합니다. 대화 기록은 요약 없이 메시지로 직접 전달되어 전체 컨텍스트(context)를 유지합니다.
교훈 (Lessons Learned)
가장 어려운 버그는 코드 버그가 아니라 프롬프트 버그입니다.
이력서 파서(resume parser)는 가끔 이력서가 아닌 것이 분명한 PDF에 대해 그럴듯해 보이는 기술을 지어내곤 했습니다. 예를 들어, 호스텔 입실 신청서에 해당 단어들이 문서 어딘가에 등장한다는 이유로 "JavaScript, Python, React"를 반환하는 식이었습니다. 해결책은 검증 코드를 더 추가하는 것이 아니라, 기술 추출을 시도하기 전에 문서 유형을 먼저 평가하도록 프롬프트를 재구성하는 것이었습니다. 2단계 프롬프트(two-step prompt) 방식은 속도는 더 느리지만 정확도는 극적으로 높아집니다.
Next.js 16 Turbopack에는 아직 문서화되지 않은 특이 사항이 있습니다.
프로젝트 루트에 middleware.ts와 proxy.ts가 모두 있으면 "Both middleware.ts and proxy.ts detected."라는 빌드 오류가 발생합니다. 에러 메시지는 충분히 명확하지만, 이 방식이 새로운 컨벤션(convention)이기 때문에 온라인상에는 관련 정보가 거의 없습니다. 문서화되지 않은 프레임워크 에러를 마주한다면, 먼저 프레임워크 버전을 확인하세요. 정답은 거의 항상 최근 릴리스에서 발생한 파괴적 변경 사항(breaking change)에 있습니다.
서버리스(serverless) 환경에서의 WebRTC는 서버가 거의 아무것도 하지 않음을 의미합니다.
직관적으로는 실시간 오디오를 위해 지속적인 서버가 필요하다고 생각하기 쉽습니다. 하지만 실제로는 OpenAI의 Realtime API를 사용할 때, 서버는 SDP 핸드셰이크(handshake, 두 번의 HTTP 요청)만 처리합니다. 그 이후의 모든 과정은 피어 투 피어(peer-to-peer)로 이루어집니다. 따라서 이 패턴에서 서버리스 함수(serverless functions)를 사용하는 것은 전혀 문제가 없습니다.
response_format: { type: "json_object" }를 사용할 때 "json"이라는 단어를 명시하지 않으면 조용히 작동이 중단됩니다.
OpenAI의 API는 프롬프트 어딘가에 "json"이라는 단어가 포함되지 않은 상태에서 JSON 모드를 사용하면 'messages' must contain the word 'json'이라는 메시지와 함께 400 Bad Request 에러를 던집니다. 리팩토링 과정에서 프롬프트 텍스트에서 해당 단어를 삭제했더니, 이전에 본 적 없는 에러와 함께 운영 환경(production)의 엔드포인트가 깨져버렸습니다. JSON 모드를 사용하는 모든 프롬프트에는 단순히 API 요구 사항을 맞추기 위해서가 아니라, 좋은 관행(good practice)으로서 "Return only a JSON object"를 추가하세요.
강제 푸시(Force-push)는 git 히스토리를 재작성하지만, GitHub의 기여자(contributor) 캐시는 느립니다.
Co-Authored-By 트레일러(trailers)를 제거하기 위해 모든 커밋을 재작성하고 강제 푸시를 한 후에도, GitHub의 기여자(Contributors) 패널에는 몇 시간 동안 이전 공동 저자가 표시되었습니다. 코드는 수정되었지만, 단지 캐시가 만료되지 않았을 뿐입니다. GitHub의 기여자 계산은 지연되어 실행되므로, 기다리는 것 외에는 방법이 없습니다.
다음 단계
One은 onee-eight.vercel.app에서 확인할 수 있습니다. 핵심 루프(core loop)는 엔드 투 엔드(end-to-end)로 작동합니다. 다음에 구축할 내용은 다음과 같습니다:
- 공유 가능한 PoW 카드 (Shareable PoW cards) — 이력서 대신 채용 지원 시 첨부할 수 있는, 검증된 기술 점수가 포함된 공개 URL
- 고용주 뷰 (Employer view) — 기업이 직무를 게시하면, One은 해당 기술 스택(stack)에서의 실제 테스트 성적에 따라 순위가 매겨진 후보자를 노출합니다.
- 점진적 재테스트 (Progressive retesting) — 매달 동일한 평가를 수행하고 기술 그래프(skill graph)를 통해 시간에 따른 기술 성장을 추적합니다.
- 팀 평가 (Team assessments) — 엔지니어링 팀 전체를 평가하고 기술 영역별로 집단적인 공백(gaps)을 찾아냅니다.
핵심 통찰은 변하지 않았습니다: 이력서는 약속입니다. 작업 증명(Proof-of-Work)은 증거입니다. 목표는 증거를 기본값(default)으로 만드는 것입니다.
Next.js 16, Supabase, OpenAI GPT-4o, OpenAI Realtime API, Tailwind CSS v4, 그리고 shadcn/ui로 구축되었습니다.
— Arish singh
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기