반복되는 UI 코드, 컴포넌트로 관리하기

제이슨·2024년 6월 2일
1
post-thumbnail

디자인 시스템 없이 피그마로 슉슉 그린 다음에 구현 먼저 하고 중복되는 부분이 많이 생기면 컴포넌트로 만들어 관리하는 방식을 사이드 프로젝트 할 때 많이 사용하는데, 처음에 개발할 때 속도가 나름(?) 빠르기 때문이다.

처음부터 각 딱! 잡고 하려면 텐션이 쳐지는 경험을 여러번 겪으며,,,

그러다 보면 비슷한 UI 요소들이 자주 등장하게 되는데, 이번 글에서는 반복되어 사용되는 UI 코드를 컴포넌트로 만들어 리팩토링한 경험을 적어보려고 한다.

문제 상황

프로젝트에서 이미지를 감싸는 배경 요소가 여러 곳에서 반복해서 사용되고 있었다.

  • 중복 코드 1
<Flex
  border={true}
  className={clsx(
    'w-full',
    'aspect-2/3',
    'rounded',
    'justify-start',
    'py-[13%]',
    'px-[5%]',
    'bg-cover bg-center',
    `items-center`,
    'bg-paper-texture',
  )}
>
  {/* 내용 */}
</Flex>
  • 중복 코드 2
<div className='relative flex w-full aspect-3/4'>
	{/* 내용 */}
</div>

이런 코드가 NewGuestBookPage, ImageUploadContainer, DetailsPage 등 여러 컴포넌트에서 발견되었다.

개선책

  1. 공통으로 사용되는 클래스를 추출한다.

    • 중복 코드 1에서 반복되는 스타일 클래스: w-full, aspect-2/3, rounded, justify-start, py-[13%], px-[5%], bg-cover bg-center, items-center, bg-paper-texture
    • 중복 코드 2에서 반복되는 스타일 클래스: relative flex w-full aspect-3/4
  2. 추출한 클래스를 바탕으로 컴포넌트를 만든다.

// 중복 코드 1 컴포넌트화
import { cva, VariantProps } from 'class-variance-authority';
import { forwardRef, HTMLAttributes } from 'react';
import Flex from './Flex';

const imageBackgroundWrapperVariants = cva('', {
  variants: {
    variant: {
      primary:
        'w-full aspect-2/3 rounded justify-start py-[13%] px-[5%] items-center',
      secondary: '',
    },
    background: {
      paper: 'bg-paper-texture bg-cover bg-center',
      none: '',
    },
  },
  defaultVariants: {
    variant: 'primary',
    background: 'paper',
  },
});

interface ImageBackgroundWrapperProps
  extends HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof imageBackgroundWrapperVariants> {
  children: React.ReactNode;
}

const ImageBackgroundWrapper = forwardRef<
  HTMLDivElement,
  ImageBackgroundWrapperProps
>(({ children, variant, className, background, ...props }, ref) => {
  return (
    <Flex
      ref={ref}
      border={true}
      className={mergeClassNames(
        imageBackgroundWrapperVariants({ variant, background, className }),
      )}
      {...props}
    >
      {children}
    </Flex>
  );
});

ImageBackgroundWrapper.displayName = 'ImageBackgroundWrapper';

export default ImageBackgroundWrapper;

여기서는 class-variance-authority 라이브러리의 cva 함수를 사용해 variants를 정의하였는데, 주어지는 변수에 따라 다른 스타일을 적용할 필요성이 있었기 때문이다.

ImageUploadContainer에서 ImageBackgroundWrapper를 사용할 때 getRootProps 함수로부터 반환된 props를 전달하고 있는데, getRootProps의 반환 값에는 ref가 포함되어 있다.

refImageBackgroundWrapper 내부의 DOM 요소에 접근하기 위해 사용하기 때문에 forwardRef로 ImageBackgroundWrapper를 감쌌다!

// 중복 코드 2 컴포넌트화
const ImageWrapper = ({ children }: { children: React.ReactNode }) => (
	<div className='relative flex w-full aspect-3/4'>
		{children}
	</div>
);

export default ImageWrapper;

중복 코드 2에 대하여 내부 이미지를 감싸는 용도로 사용될 ImageWrapper 컴포넌트도 간단히 만들었다.

cva를 사용하지 않은 이유는 현재 프로젝트 내부에서 사용되는 이미지 표시 비율이 모두 가로 3 세로 4였기 때문이고, 당장 변수로 관리할 부분이 없었기 때문이다.

적용

이제 만들어진 컴포넌트들을 기존 코드에 적용해 보자.

async function NewGuestBookPage() {
  // ...
  return (
    <>
      {/* ... */}
      <ImageBackgroundWrapper>
        <ImageWrapper>
          <Image
            src={imageUrl}
            fill
            alt={clsx(nickname, '님의 사진')}
            className='object-cover drop-shadow-lg'
          />
          <TimeStampLayer date={createdAt} />
        </ImageWrapper>
	  </ImageBackgroundWrapper>
      {/* ... */}
    </>
  );
}
function ImageUploadContainer() {
  // ...
  return (
    <ImageBackgroundWrapper
      {...getRootProps({ role: 'button' })}
      className={clsx(
        'row-start-3',
        'row-span-8',
        'mt-7',
        `justify-${file ? 'start' : 'center'}`,
      )}
      background={file ? 'paper' : 'none'}
    >
      {/* ... */}
    </ImageBackgroundWrapper>
  );
}

기존에 반복되던 스타일 코드들이 깔끔하게 ImageBackgroundWrapperImageWrapper로 대체되었다.

컴포넌트 이름을 통해 역할도 명확히 드러난 것처럼 보인다 🤔

정리

실제 프로젝트에서 마주한 코드 중복 문제를 컴포넌트로 관리하는 과정을 살펴보았다.

반복되는 UI 패턴을 공통 컴포넌트로 분리하면 코드의 가독성과 유지보수성이 높아진다. 더하여, 디자인의 일관성도 쉽게 유지할 수 있게 되었다.

profile
계속 읽고 싶은 글을 쓰고 싶어요 ☺

0개의 댓글