GitHub 팩트폭격기 'Code Truthbomb' 개발기: Next.js 16과 AI로 동료의 멘탈을 부숴보자
목차
독설가 시니어가 내 깃허브를 본다면
개발자라면 누구나 자신의 코드에 대한 자부심과 동시에 부끄러움을 가지고 있습니다. 늦은 밤 급하게 커밋한 스파게티 코드, 1년 전의 내가 짠 이해할 수 없는 로직, 그리고 관리되지 않는 수많은 토이 프로젝트 레포지토리들...
문득 이런 생각이 들었습니다. "실리콘밸리의 독설가 시니어 개발자가 내 깃허브를 본다면 뭐라고 할까?"
점잖은 코드 리뷰 말고, 정말 뼈를 때리는 적나라한 피드백을 듣고 싶지 않나요? 그 변태적인(?) 호기심에서 Code Truthbomb 프로젝트가 시작되었습니다. 이 서비스는 단순히 깃허브 통계를 보여주는 것을 넘어, 생성형 AI(Llama 3) 페르소나를 활용해 개발자의 커밋 이력과 언어 사용 패턴을 분석하고, 아주 시니컬하고 유머러스한 한국어 멘트로 사용자를 '로스팅(Roast)'합니다.
단순한 유머 서비스처럼 보이지만, 그 이면에는 Next.js 16의 최신 기능, Groq의 초고속 추론, 그리고 Zero-runtime CSS를 활용한 효율적인 스타일링 전략이 녹아있습니다. 오늘 포스팅에서는 이 서비스를 만들며 겪었던 기술적 고민들을 공유해보려 합니다.
로그인을 안 받기로 한 것
사용자 경험(UX)의 핵심은 "빠르고, 직관적이고, 재밌어야 한다" 였습니다.
1) No Login, Just Type
가장 먼저 고민한 것은 진입 장벽입니다. OAuth 로그인을 강제하면 사용자는 귀찮아서 이탈합니다. 단순히 username만 입력하면 Octokit을 통해 공개된 깃허브 데이터를 긁어오도록 구현했습니다. 덕분에 누구나 3초 만에 자신(혹은 친구)의 깃허브를 분석해볼 수 있습니다.
2) 점수별 동적 테마 (Dynamic Themes)
AI가 매긴 점수에 따라 UI 분위기가 극적으로 바뀝니다.
- 0~30점 (Fire): 지옥불 테마. 비난의 강도가 가장 높습니다.
- 31~70점 (Stone): 돌처럼 굳은 표정의 차가운 테마.
- 71~100점 (Neon): 사이버펑크 갓-티어(God-tier) 테마.
단순히 텍스트만 바뀌는 것이 아니라, 배경 인터랙션과 색상 팔레트 전체가 점수에 반응하여 사용자에게 시각적인 피드백을 강렬하게 전달합니다.
무엇으로 만들었나
토이 프로젝트일수록 기술 선정에 명확한 이유가 있어야 배울 점이 있다고 생각합니다.
-
Next.js 16 (App Router): 최신 프론트엔드 트렌드를 따라잡고, 서버 사이드 로직(OpenAI/Groq API 호출, GitHub API 중계)을
Route Handlers로 안전하게 처리하기 위해 선택했습니다. 특히 서버 컴포넌트와 클라이언트 컴포넌트의 경계를 명확히 하여 번들 사이즈를 최적화했습니다. -
Groq SDK (Llama 3.3 70B): 가장 중요한 결정이었습니다. 처음엔 GPT-4를 고려했으나, "재미"를 위한 서비스에서 5초 이상의 대기 시간은 치명적입니다. **Groq LPU(Language Processing Unit)**를 도입하여 추론 속도를 획기적으로 줄였습니다. 거의 실시간에 가까운 속도로 장문의 팩트폭력 텍스트를 생성해냅니다.
-
Vanilla Extract: CSS-in-JS의 편리함을 누리면서도 런타임 오버헤드가 없는 Zero-runtime 솔루션입니다. 특히 TypeScript와의 궁합이 환상적이라, 위에서 언급한 '점수별 테마'를 구현할 때 타입 안전성을 보장하며 스타일을 분기 처리하는 데 큰 도움이 되었습니다.
-
Framer Motion: AI가 분석하는 동안의 긴장감을 조성하는 로딩 애니메이션, 결과 카드가 등장할 때의 타격감을 구현하기 위해 사용했습니다.
예의 바른 모델을 독하게 만들기
초기 Llama 3 모델은 너무 예의가 발랐습니다. "코드가 좀 아쉽네요, 더 노력해보세요" 정도의 따뜻한 조언은 우리 서비스의 취지와 맞지 않았습니다.
AI 페르소나를 확실하게 주입하기 위해 시스템 프롬프트를 수십 번 깎았습니다.
- Role 부여: "너는 실리콘밸리의 엘리트 시니어 개발자다. 무능한 코드를 혐오한다."
- Tone & Manner: "4chan이나 Reddit의 기술 유머를 섞어라. 한국어 인터넷 은어(코딩 호소인, 복붙 장인 등)를 적극 사용해라."
- Format 강제: JSON 출력을 강제하여 프론트엔드에서 파싱하기 쉽게 만들었습니다.
// 실제 프롬프트 일부
const systemPrompt = `
You are a toxic, cynical, elite Senior Developer...
Your goal is to be **EXTREMELY BRUTAL**, mean, and technically accurate.
Use Korean internet slang (e.g., 코딩 호소인, 복붙 장인) and be very aggressive.
`;이 과정에서 AI가 가끔 JSON 형식을 깨뜨리는 할루시네이션(Hallucination) 문제가 있었는데, Groq API의 response_format: { type: "json_object" } 옵션을 켜고, 프롬프트 레벨에서도 스키마를 엄격하게 정의하여 해결했습니다.
테마를 모델에게 고르게 한 실수
이 프로젝트에서 뒤늦게 알아챈 설계 실수가 하나 있습니다.
앞서 점수 구간별로 테마가 바뀐다고 썼습니다. 030점은 불타는 붉은색, 3170점은 무채색, 71~100점은 네온. 문제는 그 매핑을 코드가 아니라 프롬프트에 적어 뒀다는 것입니다.
모델에게 요구하는 JSON 스키마가 이렇게 생겼습니다.
{
"score": number (0-100, where 0 is garbage and 100 is god-tier),
"level": string (e.g., "Script Kiddie", "StackOverflow Copy-Paster"),
"theme": "fire" | "stone" | "neon" (based on score: 0-30 -> fire, 31-70 -> stone, 71-100 -> neon),
"summary": "...",
"roast_content": "...",
"language_roast": "..."
}
theme 필드 설명에 구간이 적혀 있는 게 보이시나요. 즉 점수를 매기는 것도 모델이고, 그 점수를 구간에 매핑하는 것도 모델입니다.
이건 명백히 코드가 해야 할 일입니다. 점수가 45점이면 무조건 stone이어야 합니다. 조건문 두 줄이면 끝나고, 틀릴 여지가 없습니다. 그런데 그걸 언어 모델에게 맡기면서 온도까지 0.8로 잡아 뒀으니, 32점을 주고 fire를 반환하는 응답이 나올 수 있습니다. 실제로 결과를 여러 번 뽑아 보면 경계 근처에서 어긋나는 경우가 있었습니다.
고치는 방법은 간단합니다. 스키마에서 theme을 빼고 코드에서 계산하면 됩니다.
// 이렇게 했어야 했다
function themeFor(score: number) {
if (score <= 30) return 'fire';
if (score <= 70) return 'stone';
return 'neon';
}
const theme = themeFor(validated.score); // 모델이 아니라 코드가 결정정리하면 이렇습니다. 모델에게는 판단이 필요한 것만 맡기고, 규칙으로 정해진 것은 코드가 쥔다. 점수를 매기는 건 판단이니 모델의 일이 맞습니다. 점수를 구간에 넣는 건 규칙이니 코드의 일입니다. 둘을 한 응답에 섞어 달라고 한 게 실수였습니다.
같은 시기에 만든 AI 운세 서비스에서도 똑같은 실수를 했습니다. 거기서는 "SSR 등급은 10% 확률로만 부여해라"라고 프롬프트에 적어 뒀는데, 모델은 확률 분포를 관리하지 않으니 지켜질 리가 없었습니다. 형식은 검증하면서 값의 규칙은 부탁으로 처리하는 것이 제가 반복한 패턴이었습니다.
로그인 없이 호출 한도를 확보하기
로그인 없이 서비스를 제공하다 보니, GitHub API의 호출 제한(Rate Limit)이 문제였습니다. 클라이언트(브라우저)에서 직접 GitHub API를 호출하면 IP당 제한에 금방 걸려버립니다.
이를 해결하기 위해 Next.js API Route를 Proxy로 활용했습니다.
- 클라이언트가
/api/roast로 요청을 보냄. - 서버(
route.ts)에서 제 개인 Access Token(process.env.GITHUB_TOKEN)을 실어 GitHub API를 호출. - 받아온 데이터를 가공하여 AI에게 전달.
이렇게 하면 훨씬 넉넉한 호출 한도(시간당 5,000회)를 확보할 수 있습니다.
여기서 한 가지 더 신경 쓴 건 토큰이 없어도 동작해야 한다는 점이었습니다. Octokit 초기화를 이렇게 해 뒀습니다.
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN, // 없으면 undefined
});GITHUB_TOKEN이 비어 있으면 Octokit은 인증 없이 동작합니다. 한도가 시간당 60회로 떨어지지만 서비스가 죽지는 않습니다. 환경변수 하나가 빠졌다고 로컬 개발이 막히거나 배포가 실패하는 게 싫었습니다. 성능은 설정으로 올리되 동작 자체는 설정에 의존하지 않게 만드는 편이 나중에 덜 고생합니다.
한 사용자를 분석하는 데 GitHub 호출은 두 번 나갑니다. 프로필 조회 한 번, 저장소 목록 한 번. 저장소는 per_page: 10으로 최근 수정순 열 개만 가져오고, 소유한 저장소만 보도록 type: "owner"를 걸어 포크한 저장소는 제외했습니다. 남의 저장소를 포크해 둔 걸 두고 "이런 걸 만들었네"라고 지적하면 그건 팩트폭력이 아니라 그냥 틀린 말이니까요.
주력 언어는 이 열 개의 주 언어를 세어서 가장 많이 나온 세 개를 뽑습니다. 저장소 안의 실제 코드 비율이 아니라 저장소 개수 기준이라는 점은 짚어 둘 만합니다. 자바스크립트 한 줄짜리 저장소 세 개와 러스트로 짠 큰 프로젝트 하나가 있으면, 이 계산은 그 사람을 자바스크립트 개발자로 봅니다. 정확도를 올리려면 저장소마다 언어 통계 API를 한 번씩 더 호출해야 하는데, 그러면 호출이 열 배로 늘어납니다. 농담 서비스에 그 비용을 쓸 이유가 없어서 개수 기준으로 남겨 뒀습니다.
런타임 값으로 빌드타임 CSS를 고르기
Vanilla Extract는 빌드 타임에 CSS 파일(css)을 생성합니다. 하지만 우리 서비스는 런타임에 받아온 AI 점수(0~100)에 따라 테마를 바꿔야 했습니다.
이 간극을 메우기 위해 @vanilla-extract/recipes를 사용했습니다. 컴포넌트의 Props(점수, 테마 등)를 CSS 변수 매핑이 아닌, 미리 정의된 레시피 변형(Variant)으로 매핑하여 해결했습니다.
// 예시 코드 (개념적)
export const cardRecipe = recipe({
base: { padding: '20px', borderRadius: '10px' },
variants: {
theme: {
fire: { background: 'linear-gradient(to bottom, #ff4d4d, #990000)' },
stone: { background: '#2d3436' },
neon: { background: 'linear-gradient(135deg, #00f260, #0575E6)' }
}
}
});이렇게 하면 TypeScript가 theme props에 들어갈 수 있는 값을 자동완성해주며, 오타로 인한 스타일 깨짐을 원천 차단할 수 있었습니다.
남을 놀리는 서비스를 만들 때의 선
만들면서 기술만큼 오래 생각한 게 하나 있습니다. 이 서비스는 남을 놀리는 도구가 될 수 있다는 점입니다.
사용자명만 넣으면 누구든 분석됩니다. 공개 정보만 쓰니 기술적으로는 아무 문제가 없지만, 동료의 계정을 넣어서 나온 결과를 단톡방에 올리는 건 다른 이야기입니다. 만든 사람이 의도하지 않았다고 해서 책임이 없어지지는 않습니다.
그래서 몇 가지를 하지 않기로 했습니다.
- 순위표를 만들지 않았습니다. 점수를 모아 랭킹을 매기면 공유의 동기가 "나 이런 결과 나왔어"에서 "쟤 이거 봐"로 바뀝니다.
- 결과를 저장하지 않습니다. 서버에 남는 게 없으니 URL로 남의 결과를 퍼 나를 수 없습니다. 공유하려면 직접 캡처해야 하고, 그 한 단계가 생각보다 큰 브레이크가 됩니다.
- 결과를 실력 평가로 포장하지 않았습니다. 최근 저장소 열 개의 겉면만 보고, 코드는 열어 보지도 않습니다. 근거가 이 정도라는 걸 서비스 안에서도 밝혀 두었습니다.
회사 계정에서 대부분의 일을 하는 개발자는 개인 계정이 비어 있어서 최하 점수가 나옵니다. 오래전에 잘 만들어 둔 프로젝트는 최근 수정순 정렬에서 밀려 아예 집계에 안 들어갑니다. 이 서비스는 그 사람의 실력에 대해 사실상 아무것도 모릅니다.
농담으로 만든 것에 이런 선을 긋는 게 과해 보일 수 있는데, 반대로 생각하면 농담이라서 더 필요한 것 같습니다. 진지한 서비스는 사용자가 알아서 경계하지만, 웃기려고 만든 건 그냥 믿기 때문입니다.
정리
이 프로젝트에서 가져갈 건 두 가지입니다.
첫째, 모델에게 규칙을 부탁하지 말 것. 점수를 매기는 판단은 모델의 일이고, 그 점수를 구간에 넣는 규칙은 코드의 일입니다. 한 응답에 섞어 달라고 하는 순간 검증할 수 없는 값이 하나 늘어납니다.
둘째, 설정이 없어도 돌아가게 만들 것. 토큰이 없으면 한도만 줄고 서비스는 그대로 도는 구조가, 결과적으로 가장 손이 덜 갔습니다.