테마를 하나 추가했을 뿐인데, API 라우트를 처음부터 다시 썼다
GitHub 커밋 기록을 시각화하는 GitStyle 프로젝트를 만들고 있습니다. 첫 번째 테마는 커밋 활동을 꽃이 피어나는 애니메이션으로 표현하는 꽃 테마였습니다.
꽃 테마를 만들면서 “나중에 다른 테마 붙이기 어렵겠다”는 생각을 하긴 했습니다. 그런데도 확장성보다 구현을 먼저 택했고, 머리카락 테마를 추가하면서 그 대가를 치룬 이야기를 담고 있습니다.
1. 문제 상황
GitStyle의 핵심 API는 GitHub 유저명을 받아 APNG 애니메이션을 생성해 반환하는 단일 라우트다.
꽃 테마만 있을 때 코드는 이렇게 생겼다.
// app/api/[username]/animation/route.ts (머리카락 테마 추가 전)
export async function GET(request: NextRequest, { params }) {
const { username } = await params;
const searchParams = request.nextUrl.searchParams;
const flower = searchParams.get("flower");
const color = searchParams.get("color");
const flowerType: FlowerType = isValidFlowerType(flower) ? flower : "default";
const flowerColor: string | undefined = isValidHexColor(color) ? color : undefined;
const apngData = await generateFlowerAPNG({
username,
quality: "low",
flowerType,
flowerColor,
});
return new NextResponse(Buffer.from(apngData), { ... });
}
flower, color라는 파라미터 이름, generateFlowerAPNG라는 함수 이름, 모든 게 꽃에 특화되어 있었다.
머리카락 테마는 파라미터부터 달랐다. 꽃 종류(flowerType) 대신 곱슬도(curliness)가 필요하고, 생성 함수도 generateHairAPNG를 써야 했다.
단순히 변수 하나를 추가하는 문제가 아니었다.
2. 내가 처음 한 생각
사실 꽃 테마를 개발할 때 이미 이 문제가 보였다.
API 라우트에 flower 파라미터를 직접 박으면서, “나중에 다른 테마 붙이려면 이 코드 다 갈아엎어야겠는데”라는 생각이 들었다. 그런데 그때는 일부러 신경을 껐다.
이유는 하나였다. 꽃 테마가 괜찮을지도 확신이 없는 상태였다.
Git 커밋 활동을 꽃으로 시각화한다는 아이디어가 실제로 쓸만한 결과물로 나올지 몰랐고, 만들어보기 전까진 알 수 없었다. 그 단계에서 확장성까지 고민하는 건 아직 오지도 않은 두 번째 테마를 위해 첫 번째 테마 완성을 늦추는 일이었다.
그래서 일단 꽃 테마에 집중했다. 확장성은 “진짜로 두 번째 테마가 필요해지면 그때 고민하자”고 미뤘다.
머리카락 테마를 만들기로 결정하고 코드를 다시 열었을 때, 예상했던 대로 문제가 기다리고 있었다.
처음에는 그냥 if/else로 해결하려 했다.
// 처음 떠올린 방향
const theme = searchParams.get("theme");
if (theme === "hair") {
const hairColor = ...;
const curliness = ...;
const apngData = await generateHairAPNG({ username, hairColor, curliness });
return new NextResponse(...);
} else {
const flowerType = ...;
const flowerColor = ...;
const apngData = await generateFlowerAPNG({ username, flowerType, flowerColor });
return new NextResponse(...);
}
이렇게 하면 당장 동작은 한다. 그런데 손이 멈췄다. 테마가 하나 더 생기면 또 else if가 붙는다. 파라미터 파싱도, 생성 함수 호출도, 각 분기마다 따로 관리해야 한다. 이미 한 번 미룬 구조 문제를 if/else로 덮으면, 세 번째 테마 때는 더 고치기 어려워진다.
3. 실제로 해 본 선택
각 테마가 “파라미터 파싱 방법”과 “생성 함수”라는 두 가지 책임을 갖고 있다는 걸 인식했다.
이 두 가지를 테마별로 묶어서 레지스트리로 관리하면, 라우트 핸들러는 어떤 테마인지 몰라도 된다.
type ThemeGenerator = (options: {
username: string;
quality: "low" | "medium" | "high";
[key: string]: unknown;
}) => Promise<Uint8Array>;
type ThemeConfig = {
generator: ThemeGenerator;
parseOptions: (params: URLSearchParams) => Record<string, unknown>;
};
const THEME_CONFIGS: Record<string, ThemeConfig> = {
flower: {
generator: generateFlowerAPNG,
parseOptions: (params) => ({
flowerType: isValidFlowerType(params.get("flower"))
? params.get("flower")
: "default",
flowerColor: isValidHexColor(params.get("color"))
? params.get("color")
: undefined,
}),
},
hair: {
generator: generateHairAPNG,
parseOptions: (params) => ({
hairColor: isValidHexColor(params.get("color"))
? params.get("color")
: undefined,
curliness: isValidCurliness(params.get("curliness"))
? params.get("curliness")
: "straight",
}),
},
};
핸들러는 이렇게 단순해진다.
const theme = searchParams.get("theme") || DEFAULT_THEME;
const themeConfig = THEME_CONFIGS[theme] || THEME_CONFIGS[DEFAULT_THEME];
const themeOptions = themeConfig.parseOptions(searchParams);
const apngData = await themeConfig.generator({
username,
quality: "low",
...themeOptions,
});
다음 테마가 생기면 THEME_CONFIGS에 항목 하나만 추가하면 된다. 핸들러 코드는 건드리지 않아도 된다.
프론트엔드도 같은 방향으로 정리했다. ThemeTabs.tsx에는 THEMES 배열로 테마 메타데이터를 한곳에서 관리하고, available: false인 테마는 “Soon” 배지와 함께 비활성화되도록 했다.
const THEMES = [
{ id: "flower", label: "Flower", available: true },
{ id: "hair", label: "Hair", available: true },
{ id: "cloud", label: "Cloud", available: false },
];
4. 결과

API 라우트 변경은 성공적이었다. 새로운 테마를 추가할 때 핸들러를 수정할 필요가 없어졌고, 각 테마의 파라미터 파싱 로직이 한곳에 모였다.
다만 꽃 테마만 있을 때 놔뒀던 코드를 전부 다시 봐야 했다. API 라우트뿐 아니라 컴포넌트들도 꽃에 특화된 인터페이스를 갖고 있었기 때문에, 머리카락 테마를 연결하면서 여러 파일을 동시에 손봐야 했다. 처음부터 theme 파라미터 하나를 공통으로 설계해뒀더라면 훨씬 수월했을 작업이었다.
5. 지금 다시 해본다면?
꽃 테마 당시 확장성을 미룬 선택 자체는 틀리지 않았다고 생각한다. 아이디어가 동작한다는 걸 먼저 증명하지 않고 구조부터 잡는 건, 결과물이 나오지 않을 수도 있는 설계에 시간을 쓰는 일이다.
그런데 머리카락 테마를 추가하면서 든 생각은 이거다.
“미루는 건 맞는데, 어디까지 미룰지를 좀 더 의식적으로 결정해야 했다.”
꽃 테마를 만들 때, 확장 가능성이 높다고 판단한 지점 — 예를 들어 URL 파라미터에 theme=flower 같은 네임스페이스를 처음부터 추가하는 것은 큰 비용 없이 할 수 있는 일이었다. 그것조차 하지 않아서, 머리카락 테마 추가 시에 API URL 구조도 함께 바꿔야 했다.
앞으로 비슷한 상황이라면 이 기준을 쓰려고 한다.
- 코어 로직: 검증 전에는 확장성보다 동작을 우선한다.
- 인터페이스 (URL, 타입 시그니처): 나중에 바꾸면 파급이 크기 때문에, 처음부터 어느 정도 여유를 남겨둔다.
확장성을 전혀 고민하지 않는 것도, 처음부터 완벽하게 설계하려는 것도 둘 다 비용이 생긴다. 어느 지점에서 투자할지를 의식하는 것이, 미루는 것 자체보다 더 중요했다.