상세페이지 구현: Conatiner - Presentational 패턴 적용기

doy·2025년 3월 16일

Dothemeet 프로젝트

목록 보기
1/2
post-thumbnail

오늘은 프로젝트에서 상세페이지를 구현하면서 코드 구조를 개선한 과정을 기록해보려 해요.
이번 프로젝트에서는 다른 팀원들이 나중에 수정할 일이 생겨도 쉽게 이해할 수 있도록 관심사가 명확히 분리된 코드를 작성하는게 저의 개인적인 목표였거든요✊

🔍 문제인식

상세페이지에서는 모임의 기본 정보부터 참여자 현황, 모임 소개글, 참여자들의 후기, 링크 공유하기, 찜하기/신청하기 기능 등등 다양한 정보와 기능을 한 페이지에 담아야했어요.




🥨 문제점

  • 상세페이지에서 보여줄 모임의 데이터, 상태, UI 관련 코드를 모두 Container 컴포넌트에서 담당하고 있었기 때문에 코드가 점점 더 복잡해짐
  • 작업이 진행됨에 따라 코드가 지나치게 길어져 어디를 고쳐야할지 찾기도 힘들고, 유지보수가 어려워질 것이라는 우려😅

처음 작성했던 코드는 이런 모습이었어요:

// 기존 코드
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 패턴'을 적용해보기로 했습니다.

Container-Presenter 패턴이란?

: ContainerPresenter로 관심사를 직관적으로 분리

Presenter Component (Presentational Component)

  • 화면에 "어떻게 보여줄지"에 집중하는 컴포넌트
  • 상태는 거의 없고, 상태를 가진다해도 데이터 상태가 아닌 UI 관련 상태만 조금(dialog 등)
    useState, useCallback 등 state 관련 훅 최소화
  • Container에서 받은 props를 통해 state를 변경

Container Component

  • 화면에 "무엇을 보여줄지"에 집중하는 컴포넌트
  • UI/스타일 관련 코드는 최소화하고 데이터와 상태를 관리
  • 데이터와 데이터 조작에 관한 함수를 만들어 관리하고 Presenter 컴포넌트에 전달

비슷한 패턴으로 '상태(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를 분리하고 나니 코드가 훨씬 깔끔해졌어요!😊

  • 데이터나 상태 관리 코드를 수정하려면 Container를 열면 되고
  • UI 관련 코드를 수정하려면 Presenter를 열면 되므로

개발 중 생산성이 높아지는 것을 경험했어요. 다른 팀원들이 코드를 볼 때도 "아, 여기는 데이터 관련 코드, 저기는 UI 관련 코드구나"라고 쉽게 이해하더라구요.

물론 이 패턴이 모든 상황에 적합한 것은 아니지만, UI와 상태관리 로직으로 복잡한 코드로 고생하고 있던 상황에서 큰 도움을 받을 수 있었던 경험이었답니다 🙌



참고)

profile
👾

0개의 댓글