본문 바로가기

개발 공부

거시적인 관점에서 코드를 짜기

프론트엔드 개발을 하다 보면, 당장 기능만 구현하는 식으로 코드를 짜기 쉽습니다.

이렇게 만든 코드는 시간이 지나거나 팀원이 바뀌었을 때 유지보수하기 어렵고, 재사용하기도 힘듭니다.

 

그래서 저는 최근에 거시적인 관점에서 코드를 구조화하는 방법을 적용했고, 그 과정을 정리해 보았습니다.

 


문제 상황

처음에는 "캐릭터 카드"를 보여주는 간단한 그리드 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/비즈니스 로직/데이터 패칭이 모두 섞여있음 → 유지보수가 어려움
  • 카드 스타일과 그리드 배치 로직이 중복될 위험이 있음

 

해결 방법

  1. 도메인 모델과 DTO 분리 – API 응답을 바로 쓰지 않고, 어댑터를 통해 도메인 모델로 변환합니다.
  2. 프레젠테이션과 컨테이너 분리 – UI는 데이터와 분리해 순수하게 렌더링만 담당합니다.
  3. 폴더 구조 정리 – 도메인, 인프라(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