
오늘은 프로젝트에서 상세페이지를 구현하면서 코드 구조를 개선한 과정을 기록해보려 해요.
이번 프로젝트에서는 다른 팀원들이 나중에 수정할 일이 생겨도 쉽게 이해할 수 있도록 관심사가 명확히 분리된 코드를 작성하는게 저의 개인적인 목표였거든요✊
상세페이지에서는 모임의 기본 정보부터 참여자 현황, 모임 소개글, 참여자들의 후기, 링크 공유하기, 찜하기/신청하기 기능 등등 다양한 정보와 기능을 한 페이지에 담아야했어요.
![]() | ![]() |
|---|
🥨 문제점
처음 작성했던 코드는 이런 모습이었어요:
// 기존 코드
import { useState, useEffect } from 'react';
...
export default function DetailContainer({id}: IDetailContainer) {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<IMoimDetail | null>(null);
useEffect(() => {
async function fetchData() {
// ... 복잡한 데이터 호출 및 상태 업데이트 로직
// 상태 관련 함수들
const [isLiked, setIsLiked] = useState(false);
const handleLikeClick = () => {
setIsLiked(prev => !prev);
}
// ... 그외의 상태관리 함수들
// JSX 코드
return (
<div className="w-full min-h-screen px-4 pb-[93px] bg-background200">
<DothemeetLogo />
<DetailShare />
<ImageBox
image={data?.image || DEFAULT_IMAGE.MOIM}
/>
// ...
작업이 진행될수록 새로운 기능이 추가되고, 컴포넌트가 추가될 때마다 코드는 점점 길어졌습니다. 나중에 수정할 코드를 찾아 스크롤을 내리고 또 내리는 저의 모습이 눈에 선했어요,,🙄
이 문제를 해결하기 위해 'Container-Presenter 패턴'을 적용해보기로 했습니다.
: Container와 Presenter로 관심사를 직관적으로 분리
Presenter Component (Presentational Component)
Container Component
비슷한 패턴으로 '상태(Stateful)-무상태(Stateless)', '클래스(Class)-함수(Functional)', '순수(Pure)-비순수(Impure)' 가 있다고 해요.
저희 팀은 이미 컨벤션으로 Container 컴포넌트를 사용하고 있기 때문에
컨벤션을 위배하지 않으면서도 직관적인 Container-Presenter 패턴을 사용하기로 했습니다.
작은 컴포넌트에서는 이 패턴을 적용하는게 오히려 복잡성을 증가시키는 원인이 될 수 있어요.
하지만 제가 작업 중인 모임 상세페이지는 다양한 컨텐츠를 담는 컴포넌트가 계속 늘어날 예정이었고,
신청하기, 찜하기 등의 기능도 추가될 예정이었기 때문에 지금 구조를 정리해두는게 좋겠다는 판단이 들었습니다.

또한 이 프로젝트에서는 SSR(서버 사이드 렌더링)을 구현할 계획이었기 때문에
page.tsx: 서버 컴포넌트, 라우팅 및 prefetch 담당container.tsx: 클라이언트 컴포넌트, 클라이언트 훅을 사용presenter.tsx: 클라이언트 컴포넌트, UI 코드이렇게 관심사를 분리해야겠다는 생각이 들었어요.
1. Container 컴포넌트 변경
기존 컨테이너에서 UI 코드를 제거하고 데이터와 상태 관리에 집중하도록 수정했어요.
// 변경 후 DetailContainer.tsx
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { QUERY_KEYS } from './detail.const';
import DetailPresenter from './DetailPresenter';
import { getMoimDetail } from '@/apis/moim';
export default function DetailContainer({ id }: IDetailContainer) {
// 데이터 페칭
const { data, isLoading, error } = useQuery({
queryKey: QUERY_KEYS.MOIM_DETAIL(id),
queryFn: () => getMoimDetail(id),
});
// 상태 관리
const [isLiked, setIsLiked] = useState(false);
const handleLikeClick = () => {
setIsLiked(prev => !prev);
// ...
};
// 참여하기 함수
const handleJoinClick = () => {
// ...
};
// 다른 데이터 관련 함수들...
// Presenter에 필요한 props 전달
return (
<DetailPresenter
data={data}
isLoading={isLoading}
error={error}
isLiked={isLiked}
onLikeClick={handleLikeClick}
onJoinClick={handleJoinClick}
// ... 기타 props
/>
);
}
2. Presenter 컴포넌트 추가
그 다음, UI 렌더링만을 담당하는 Presenter 컴포넌트를 추가했어요.
// 추가된 DetailPresenter.tsx
import { Header } from '@/components/detail/Header';
// ...
function DetailPresenter({
data,
masterUser,
participants,
reviews,
isJoining,
isLiked,
onJoin,
onLikeToggle,
actionLabel,
disabled,
className,
}: IDetailPresenterProps) {
if (!data || !masterUser) {
return null;
}
return (
<div className="w-full min-h-screen mx-auto px-4 pb-[92px] bg-background200 xs:max-w-screen-xs sm:max-w-screen-sm md:max-w-screen-md lg:max-w-screen-lg">
<Header />
<DetailShare />
<ImageBox image={data.image} />
<DetailInfo
// 필요한 props ...
/>
<DetailParticipants
// 필요한 props ...
/>
<DetailContent
// 필요한 props ...
/>
<DetailHost
// 필요한 props ...
/>
<DetailReview
// 필요한 props ...
/>
<FloatingBar
// 필요한 props ...
/>
</div>
);
}

이렇게 Container와 Presenter를 분리하고 나니 코드가 훨씬 깔끔해졌어요!😊
개발 중 생산성이 높아지는 것을 경험했어요. 다른 팀원들이 코드를 볼 때도 "아, 여기는 데이터 관련 코드, 저기는 UI 관련 코드구나"라고 쉽게 이해하더라구요.
물론 이 패턴이 모든 상황에 적합한 것은 아니지만, UI와 상태관리 로직으로 복잡한 코드로 고생하고 있던 상황에서 큰 도움을 받을 수 있었던 경험이었답니다 🙌
참고)