bkdragon's log

컴포넌트 분리하기 전략 본문

React

컴포넌트 분리하기 전략

bkdragon 2023. 5. 24. 17:54

‘컴포넌트 분리하기’

 

참으로 어려운 문제이다. 구글에 검색을 하면 다른 내용의 다양한 글이 나오는데 이는 아직까진 왕도가 없다는 뜻일 것이다. 그래서 나도 우선 나의 전략을 짜기로 했다. 후에 현업에 가서 새로운 인사이트를 얻고 다양한 코드를 보고 지속적으로 발전시킬 계획이다.

 

본격적인 글을 작성하기에 앞서 내용이 주관적일 수 있고 언제든 변경될 수 있음을 알린다.

1. 관심사의 분리

비슷한 관심사를 가지는 코드들끼리 모아두는 것이다. 이것은 model과 view의 분리로 생각할 수도 있다.

예를 들어 게시판 상세 페이지를 만드는 상황이다. 게시판 상세 페이지에는 다양한 데이터가 필요할 것이다. (글의 제목, 작성자, 생성한 날짜, 댓글, 이미지 등등) 이런 데이터를 모아두는 컴포넌트와 데이터를 단순히 화면에 렌더링하는 컴포넌트로 구분하는 것이다.

 

사실 이부분은 내가 애용하고 있는 Container/presenter 패턴이다.

 

데이터 패칭이나 패칭해온 데이터를 다루는 로직들이 모여있다. 이를 view 역할을 하는 컴포넌트로

넘겨준다.

BoardDetail.container.tsx

import BoardDetailUI from "./BoardDetail.presenter";

 export default function BoardDetail() {
  const router = useRouter();
  const { loading, data, error } = useQuery<...>(...) // 상세 데이터 조회

  const [deleteBoard] = useMutation<...>(...) // 삭제하는 메서드
  const [likeBoard] = useMutation<...>(...) // 좋아요를 증가시키는 메서드
  const [disLikeBoard] = useMutation<...>(...) // 싫어요를 증가시키는 메서드

    const handleMove : MouseEventHandler<HTMLButtonElement> = () => {
    router.push("/boards");
  };

  return (
    <BoardDetailUI
      data={data}
      handleDelete={deleteBoard}
      handleMove={handleMove}
      handleLike={likeBoard}
      handleDisLike={disLikeBoard}
    />
  );
}

 

더 나아가서 style과 관련된 코드를 다른 파일로 분리 했다.

BoardDetail.presenter.tsx

import * as S from "./BoardDetail.styles";
export default function BoardDetailUI({
  data,
  handleDelete,
  handleMove,
  handleMoveEdit,
  handleLike,
  handleDisLike
} : {
  data : Pick<IQuery, "fetchBoard">;
  handleDelete : MouseEventHandler<HTMLButtonElement>;
  handleMove : MouseEventHandler<HTMLButtonElement>;
  handleLike : MouseEventHandler<HTMLDivElement>;
  handleDisLike : MouseEventHandler<HTMLDivElement>
}) {

  return (
    <S.DetailContainer>
     // ... 생략, Container에 받은 데이터와 함수를 적절히 사용함.
    </S.DetailContainer>
  );
}

이 패턴의 장점은 원하는 파일로 이동이 쉽다는 점이다. 개발 과정에서 게시판 상세 페이지에서 보여주는 데이터의 이상이 있을 때 코드 에디터에서 ‘BoardDetail.container’ 라고 검색해서 들어가서 수정하면 된다. 이는 개발 시간을 상당히 단축 시켜준다. (윈도우 단축키 : ctrl + p)

 

단점은 모든 컴포넌트를 Container와 Presenter로 분리하는 것이 번거로울 수 있다. 사실 이부분은 스스로의 기준, 팀의 기준이 확실하면 된다. 너무 작은 사이즈의 컴포넌트는 한 파일로 관리하자는 약속, 기준이 있으면 된다. (난 그렇게 하고 있다.)

2. 컴포넌트의 복잡성

이는 간단하다. 한 컴포넌트가 너무 커지고 복잡해지는 것을 막는 것이다. 게시판 리스트를 보여주는 컴포넌트가 있다고 하자. (위의 1번 전략을 적용했다면 BoardList.presenter.tsx 라는 파일일 것이다.) 이 컴포넌트 내부에서는 여러가지 글의 리스트를 보여주기도 해야하고 광고가 있다면 광고도 보여줘야하고 추천 글이 있다면 추천 글도 보여줘야 한다. 이게 모두 BoardList.presenter.tsx 안에 다 있다면 복잡해질 것이다.

 

BoardList.presenter.tsx

import React from "react";
import * as S from "./List.style";

export default function BoardListUI({
  data,
  page,
  setPage,
  lastPage
} : {
  data : Pick<IQuery, "fetchBoards">;
  page : number;
  setPage : React.Dispatch<React.SetStateAction<number>>;
  lastPage : number;
}) {

  return (
    <S.Container>
      {/*  베스트 게시글 */}
            <Best />
            {/*  광고 */}    
      <Advertisement />
            {/*  게시글 리스트 */}
      <S.ListWrapper>
        <S.List>
          {data?.fetchBoards.map((e, i) => (
            <S.Element key={e._id}>
              <div>{i + 1}</div>
              <S.ElementTitle id={e._id} onClick={handleMoveDetail}>
                {e.title}
              </S.ElementTitle>
              <div>{e.writer}</div>
              <div>{getDate(e.createdAt)}</div>
            </S.Element>
          ))}
        </S.List>
      </S.ListWrapper>
            {* 추가적인 무언가 *}
      <AdditionalSomething />
    </S.Container>
  );
}

여기서 분리한 Best, AdvertiseMent, AdditionalSomething 컴포넌트가 다른 곳에서도 쓰인다면 다음에 소개할 전략까지 포함된 예시가 된다. 근데 그렇지 않더라도 코드가 한결 깔끔해지는 장점을 지닌다.

 

3. 재사용성

간단하다. 2번 이상 반복된다면 분리하고 보자. 이미지를 슬라이드로 보여주는 기능은 인덱스 페이지에도 게시글 상세 페이지에도 게시글 작성 페이지에도 다양한 페이지에서 필요할 것이다.

 

ImageSlider.tsx

import React from 'react';

import styled from '@emotion/styled'

import Slider from 'react-slick'

interface ImageSliderProps {
    images : string[]
    width? : number;
    height? : number;
}

const ImageSliderContainer = styled.div<{
    width? : number;
    height? : number;
}>`
    width : ${(props) => props.width ? `${props.width}px` : '100px'};
    height : ${(props) => props.height ? `${props.height}px` : '100px'};
    overflow: auto;
`

const ImageSlider : React.FC<ImageSliderProps> = ({
    images,
    width,
    height
}) => {

    const settings = {
        dots: true,
        infinite: true,
        speed: 500,
        slidesToShow: 1,
        slidesToScroll: 1
    };

    if(!images?.length) {
        return null;
    }

    return (
        <ImageSliderContainer width={width} height={height}>
            <Slider {...settings}>
                {images.map((url, i) => (
                    <img src={`https://storage.googleapis.com/${url}`} key={i} />
                ))}
            </Slider>
        </ImageSliderContainer>
    );
};

export default ImageSlider;

(그리고 사실 이것도 1번 전략을 적용할 수 있을 것이다. 하지만 사이즈가 작기 때문에 그냥 작성했다. )

우선 전략은 이 정도로 정리 할 수 있을 것 같다. 난 이제 분리과정에서 내 스스로 필요하다고 생각하는 부분에 대해 이야기 해보려고 한다.

 

props 타입 정의

컴포넌트를 분리하다보면 props drilling은 자연히 발생하는 문제이다. 완벽한 해결은 없다고 본다. global state management library들이 좋게 개발이 되고 있지만 고작 한 dept에서 사용해야하는지는 고민해 볼 문제이다.

 

내가 말하는 props 타입 정의는 컴포넌트를 분리할 때 타입 정의의 과정이 먼저 이뤄져야 한다는 것이다. 예를 들어 위의 ImageSlider 컴포넌트를 분리할 때 나는 인터페이스를 먼저 작성했다.

interface ImageSliderProps {
    images : string[]
    width? : number;
    height? : number;
}

계획이 없는 행동은 길을 잃을 수 있다. 인터페이스는 일종의 계획서이다.

 

문자열이 들어간 images를 배열을 받아오기 때문에 image를 화면에 렌더링 해야겠다고 실행 할 수 있고, width와 height를 받아오기에 사용처마다 다른 width를 적용해야겠다는 실행을 할 수 있다.

 

계획서 없이 마구잡이로 만들다보면 갑자기 이미지를 업로드하는 기능이 이미지 슬라이드 컴포넌트에 들어갈지도 모르는 일이다.

 

데이터 사용처

1번 전략의 관심사의 분리를 위해 model과 view 구분 지어 컴포넌트를 설계해왔다. 이 전략으로 꽤나 보기 좋은 코드를 얻었다. 그럼 이런 예를 들어보자.

 

메인페이지가 있다. 이 메인 페이지에는 이 서비스에 대한 정보, 여러가지 광고, 다양한 게시글 등이 나열되어있다. 그리고 한켠에 로그인을 했다면 이 로그인을 한 유저에 대한 정보가 간략하게 제공이 된다. 그리고 이 유저 정보를 보여주는 부분을 다른 컴포넌트로 분리하였다.(복잡해지는 것을 막기 위해.) 이 곳 외에 메인페이지에서 유저 관련 데이터를 보여주는 부분은 전혀 없다. 자 이 상황이면 유저 정보를 패칭하는 컴포넌트 어디가 되어야 할까?

 

답이 정해진 문제는 아니다. 내 결론은 분리된 컴포넌트에서 패칭하자! 이다. 메인 페이지에서 사실상 사용되지 않는 데이터를 메인페이지의 내부에 존재하는 컴포넌트가 사용한다는 이유로 메인페이지에서 데이터 패칭을 한다면 분리된 컴포넌트는 메인페이지에 의존성이 생겨버린다. 만약 나중에 새로운 페이지가 개발되고 그 페이지에서도 유저 데이터를 보여주는 작은 UI가 생긴다면 새로운 코드를 짜야할 것이다.

 

 

 

일단 여기까지 현재 나의 결론이다. 내용의 추가나 변화가 있을 수 있음을 다시 한번 알린다.

'React' 카테고리의 다른 글

커링과 HOC  (0) 2023.05.31
[React-Query] 캐싱, 헷갈리는 부분  (0) 2023.05.26
React-Query useQuery를 Hook으로 사용하기  (0) 2023.05.02
[React Query] select  (0) 2023.03.11
[React Query] refetchOnMount  (0) 2023.03.04