프론트엔드 개발을 하다 보면, 당장 기능만 구현하는 식으로 코드를 짜기 쉽습니다.
이렇게 만든 코드는 시간이 지나거나 팀원이 바뀌었을 때 유지보수하기 어렵고, 재사용하기도 힘듭니다.
그래서 저는 최근에 거시적인 관점에서 코드를 구조화하는 방법을 적용했고, 그 과정을 정리해 보았습니다.
문제 상황
처음에는 "캐릭터 카드"를 보여주는 간단한 그리드 UI를 만들면 된다고 생각했습니다.
그래서 API에서 받은 데이터를 그대로 카드 컴포넌트에 넣고, 그걸 그리드로 배치하는 방식으로 구현했습니다.
// 초안 코드
export default function CharacterGrid({ characters }: { characters: any[] }) {
return (
<div className="grid grid-cols-4 gap-4">
{characters.map(c => (
<div key={c.id} className="border p-3">
<div>{c.title}</div>
{c.isNew && <span>NEW</span>}
{c.isHot && <span>HOT</span>}
</div>
))}
</div>
);
}
문제는 이 방식이 재사용성과 확장성이 전혀 없다는 겁니다.
- characters의 데이터 구조가 API DTO에 강하게 묶여있음 → API가 바뀌면 컴포넌트도 깨짐
- UI/비즈니스 로직/데이터 패칭이 모두 섞여있음 → 유지보수가 어려움
- 카드 스타일과 그리드 배치 로직이 중복될 위험이 있음
해결 방법
- 도메인 모델과 DTO 분리 – API 응답을 바로 쓰지 않고, 어댑터를 통해 도메인 모델로 변환합니다.
- 프레젠테이션과 컨테이너 분리 – UI는 데이터와 분리해 순수하게 렌더링만 담당합니다.
- 폴더 구조 정리 – 도메인, 인프라(API), UI, feature 단위로 나누어 의존성을 관리합니다.
리팩터링 결과
1) 도메인 모델 & 어댑터
// domain/models/Character.ts
export interface Character {
id: number;
name: string;
isAdult: boolean;
isNew: boolean;
isHot: boolean;
}
// infra/dto/character.dto.ts
export interface CharacterDTO {
id: number;
title: string;
isAdult: boolean;
isNew: boolean;
isHot: boolean;
}
// infra/adapters/character.adapter.ts
import { CharacterDTO } from "../dto/character.dto";
import { Character } from "../../domain/models/Character";
export const toCharacter = (dto: CharacterDTO): Character => ({
id: dto.id,
name: dto.title,
isAdult: dto.isAdult,
isNew: dto.isNew,
isHot: dto.isHot,
});
- 이제 API 응답이 바뀌더라도 UI는 그대로 두고 어댑터만 수정하면 됩니다.
2) 프레젠테이션 컴포넌트
// ui/CharacterCard.tsx
type CharacterCardProps = {
name: string;
badges?: Array<"adult" | "new" | "hot">;
onClick?: () => void;
};
export default function CharacterCard({ name, badges = [], onClick }: CharacterCardProps) {
return (
<button className="rounded-2xl p-3 border w-[163px]" onClick={onClick}>
<div className="font-semibold">{name}</div>
<div className="flex gap-1 mt-1">
{badges.map(b => (
<span key={b} className="text-xs">{b}</span>
))}
</div>
</button>
);
}
3) 컨테이너 컴포넌트
// features/character/CharacterGrid.tsx
import CharacterCard from "../../ui/CharacterCard";
import { useQuery } from "@tanstack/react-query";
import { getCharacters } from "../../infra/apis/character.api";
import { toCharacter } from "../../infra/adapters/character.adapter";
export function CharacterGrid() {
const { data } = useQuery({
queryKey: ["characters"],
queryFn: async () => {
const dtoList = await getCharacters();
return dtoList.map(toCharacter);
}
});
return (
<div className="grid [grid-template-columns:repeat(auto-fit,minmax(154px,1fr))] gap-4 max-w-[688px] mx-auto">
{data?.map(c => (
<CharacterCard
key={c.id}
name={c.name}
badges={[
...(c.isAdult ? ["adult"] : []),
...(c.isNew ? ["new"] : []),
...(c.isHot ? ["hot"] : []),
]}
/>
))}
</div>
);
}
- 먼저 설계하면, 팀원이 바뀌어도 빠르게 적응할 수 있습니다.
- 도메인 모델을 따로 두면 API 변경에도 안전합니다.
- 프레젠테이션/컨테이너 분리로 UI를 디자인 시스템처럼 재사용할 수 있습니다.
- 폴더 구조를 역할 기준으로 나누면 코드 가독성이 크게 올라갑니다.
'개발 공부' 카테고리의 다른 글
| Hoisting(호이스팅) (0) | 2025.09.21 |
|---|---|
| 웹 서버(Web Server), 웹 애플리케이션 서버(WAS) (0) | 2025.09.20 |
| try-catch & then-catch (1) | 2025.08.03 |
| Pub/Sub (3) - Next.js + Zustand (0) | 2025.06.30 |
| Pub/Sub (2) - Angular + RxJS (0) | 2025.06.29 |