재사용성을 높이는 리액트 컴포넌트 설계 패턴

jinew·2025년 3월 26일
0

🥝 React

목록 보기
4/4
post-thumbnail

최근 팀 프로젝트에서 도서 판매 웹사이트를 만들었다. 내가 작업할 컴포넌트를 메인 페이지, 목록 페이지, 상세 페이지 등 여러 곳에서 같은 UI 요소를 반복해서 사용해야 했다. 이런 상황에서 컴포넌트 재사용성을 높이기 위한 설계 방법을 고민했고, 팀원들이 소위 말하는 '딸깍' 으로 복잡한 과정 없이 UI를 즉각 적용할 수 있게끔 설계하는 것에 집중했다. 오늘은 그 과정에 대한 회고를 하고자 한다.


📚 재사용 가능한 컴포넌트가 왜 중요할까?

프로젝트가 커질수록 반복되는 UI 패턴이 많아지는데, 이 때 재사용 가능한 컴포넌트를 잘 설계해 두면

1. 코드 중복 방지 : 같은 기능을 여러 번 구현하지 않아도 됨
2. 일관된 디자인 유지 : 모든 페이지에서 동일한 스타일과 기능을 제공
3. 유지보수가 쉬워짐 : 한 곳만 수정하면 모든 곳에 적용
4. 개발 속도가 빨라짐 : 이미 만들어진 컴포넌트를 활용하면 새 기능 개발이 용이함

와 같은 장점이 생긴다!


🧩 재사용 가능한 컴포넌트 설계 원칙

프로젝트에서 실제로 적용한 몇 가지 원칙이다.

1. 선택적 props 활용하기

컴포넌트가 필요한 정보만 받을 수 있도록 props를 설계하는 것이 중요하다. 필수 props와 선택적 props를 구분해서 다양한 상황에 대응할 수 있게 만들었다.

function CardSectionLayout({
  title,
  path,
  children,
  className,
}: SectionContainerProps) {
  return (
    <section className={mergeClasses('max-w-[1000px] mx-auto', className)}>
      <h2 className='ml-2 font-semibold text-lg mb-8'>
        {path ? (
          <Link href={path} className='flex items-center gap-1'>
            {title} <ChevronRight className='w-4' />
          </Link>
        ) : (
          title
        )}
      </h2>
      {children}
    </section>
  );
}

이 컴포넌트는 titlechildren은 필수지만, pathclassName은 선택적이다. path가 있으면 제목을 클릭 가능한 링크로 만들고, 없으면 그냥 텍스트로 표시한다.


2. 조건부 렌더링 활용하기

상황에 따라 다른 UI를 보여주기 위해 조건부 렌더링을 활용했다. 위 코드에서 볼 수 있듯이, path prop이 있을 때만 제목을 링크로 만들고 화살표 아이콘도 함께 표시한다.


3. 스타일 충돌 방지하기

Tailwind CSS를 사용할 때 클래스 충돌 문제를 해결하기 위해 mergeClasses 유틸리티 함수를 만들었다.

import clsx, { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

/**
 * 주어진 클래스들을 병합하여 반환하는 함수
 * `clsx`로 클래스명을 처리하고, `twMerge`로 중복된 Tailwind 클래스를 병합
 *
 * @param {...ClassValue[]} inputs - 병합할 클래스들
 * @returns {string} 병합된 클래스 문자열
 */
export function mergeClasses(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}

이 함수는 clsxtailwind-merge 라이브러리를 사용하여 기본 클래스와 사용자 지정 클래스를 충돌 없이 병합해 준다. 기본 스타일은 유지하면서 필요한 부분만 커스터마이징할 수 있게 되었다! 😎



🎨 실제 프로젝트 적용 사례

메인 페이지에서는 여러 섹션(베스트셀러, 추천 도서 등)이 있는데, 같은 레이아웃 컴포넌트를 재사용했다.

// 베스트 셀러 섹션
<CardSectionLayout
  title='베스트셀러'
  path='/book-list/best'
  className='w-[85%] mt-16 md:mt-36'
>
  <BestSellerCarousel />
</CardSectionLayout>

// 추천 도서 섹션
<CardSectionLayout
  title='이 주의 추천 도서'
  className='w-[85%] my-16 md:my-36'
>
  <RecommendedBooksCarousel />
</CardSectionLayout>

베스트셀러 섹션에는 path를 제공하여 "더보기" 링크를 활성화했고, 추천 도서 섹션에는 링크가 필요 없어서 path를 생략했다. 두 섹션 모두 className을 통해 위치와 여백을 조정했다.

📱 반응형 디자인도 고려하기

재사용 컴포넌트는 다양한 화면 크기에서도 잘 작동해야 한다. 캐러셀 컴포넌트를 예로 들면,

function BookCarouselLayout(props: { bookList: CardForCarousel[] }) {
  const carouselOptions = {
    slidesToScroll: SLIDES_TO_ONE_SCROLL.SMALL,
    breakpoints: {
      '(min-width: 640px)': { slidesToScroll: SLIDES_TO_ONE_SCROLL.MEDIUM },
      '(min-width: 768px)': { slidesToScroll: SLIDES_TO_ONE_SCROLL.LARGE },
    },
  };

  return (
    <div>
      <Carousel opts={carouselOptions}>
        <CarouselContent className='-ml-4 md:-ml-6'>
          {props.bookList?.map((book) => {
            return (
              <CarouselItem
                key={book.id}
                className='basis-1/2 sm:basis-1/3 md:basis-1/4 pl-4 md:pl-6'
              >
                <BookCard {...book} />
              </CarouselItem>
            );
          })}
        </CarouselContent>
        <CarouselPrevious className='-left-4 md:-left-12 w-7 h-7 md:w-8 md:h-8' />
        <CarouselNext className='-right-4 md:-right-12 w-7 h-7 md:w-8 md:h-8' />
      </Carousel>
    </div>
  );
}

화면 크기에 따라 다른 수의 카드를 보여주고, 화살표 버튼의 크기와 위치도 조정했다. 이렇게 하면 모바일부터 데스크탑까지 모든 화면에서 최적의 사용자 경험을 제공할 수 있다 👍

🔍 데이터 독립성 확보하기

재사용 컴포넌트는 데이터 소스에 독립적이어야 한다. BookCarouselLayout 컴포넌트는 bookList prop을 통해 데이터를 받기 때문에, 베스트셀러 목록이든 추천 도서 목록이든 관계없이 동일한 UI로 표시할 수 있다.
이렇게 데이터와 UI를 분리하면 컴포넌트 재사용성이 높아지고, 다양한 데이터 소스와 연동하기 쉬워진다.



🌟 회고

프로젝트를 진행하면서 재사용 가능한 컴포넌트를 설계하는 것이 얼마나 중요한지 깨달았다. 처음에는 시간이 더 걸리는 것 같지만, 결국 개발 속도를 높이고 일관된 UI를 유지하는 데 큰 도움이 된다는 걸 느꼈다 !
특히 mergeClasses 같은 유틸리티 함수는 작은 코드지만 큰 효과를 발휘했다. 이런 작은 노력들이 모여 유지보수하기 쉬운 코드베이스를 만들어 냈다.

팀원들이 "그냥 이렇게만 쓰면 베스트 셀러 캐러셀이 나온다고요? 엄청 편하다" 라고 해주셨을 때의 뿌듯함을 기억하며 앞으로도 재사용 가능한 컴포넌트를 곰곰히 생각하며 구현해야겠다!

profile
멈추지만 않으면 도착해 🛫

0개의 댓글

관련 채용 정보