기술

컴포넌트를 잘 만드는 방법(리액트)

안양사람 2022. 7. 16. 00:53
728x90
SMALL

컴포넌트란

프론트엔드 개발자라면 컴포넌트라는 말을 들어봤을 것이다. 컴포넌트가 뭘까?? 간단하게 말하면 컴포넌트는 독립적인 단위 모듈이다. 리액트를 예시로 들면 컴포넌트를 여러개 만들고 이걸 조합해서 하나의 페이지를 만든다. 이게 기본적인 설명의 전부다.

https://hanamon.kr/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-component%EB%9E%80/

컴포넌트를 만드는 방법

간단하게 생각해보기

정책, 디자인, 백엔드 등의 사전작업이 모두 이루어졌고 새롭게 프론트엔드 프로젝트를 시작한다고 가정해보자. 이제 컴포넌트 관점에서 어떻게 진행을 해야할지 생각해보자. 아무리 작은 규모의 프로젝트라고 할지라도 버튼, 인풋, 모달 등은 존재한다. 그리고 이것들은 많은 화면에서 비슷한 ui를 가진다. 이런 공통 컴포넌트들을 먼저 구현하면 큰 컴포넌트를 만들 때 훨씬 편하게 구현할 수 있다. 보통 요즘은 디자인 컴포넌트라고 불린다. 그리고 스토리북을 이용해 ui 테스트를 하기도 한다. 공수에 비해서 얻는 장점이 크기 때문에 많은 회사에서 사용하고 있다.

 

공통 컴포넌트를 먼저 구현해야하는 이유가 뭘까?? 재사용이 가능하기 때문에?? 그것도 맞다. 컴포넌트라는 관점이 아닌 개발이라는 관점으로 확장시켜서 생각해보자. 처음부터 큰 구현사항을 짧은 시간안에 구현할 수는 없다. 그렇기 때문에 우선순위가 중요하다. 막무가내로 개발하다보면 그 프로젝트는 망하게 된다. 그러면 어떤 것부터 개발해야할까?? 다시 컴포넌트의 정의에 대해서 생각해보자. 왜 컴포넌트 기반의 라이브러리가 유행하게 되었을까?? 컴포넌트는 독립적인 단위 모듈이다. 독립적이라는 것이 중요하다. 인터페이스의 영향을 받기는 하지만 각각이 독립적이다. 개발을 설계할 때는 작고 독립적인 단위부터 개발해야 한다. 이게 전체 설계를 하기전에 독립적인 것들을 개발해야 한다고 말하는 것은 아니다. 오히려 전체적인 맥락을 파악해야 한다. 디자인 파일을 보고 위에서부터 아래까지 페이지를 만드는것이 아니라 작은 단위로 쪼개서 생각을 해야한다는 것이다. (코드스피츠 강사님도 항상 작고 간단한것부터 시작하라고 말씀하셨다.)

 

디자인 컴포넌트는 사실 나누기 쉽다. 이미 디자이너들이 나눠놓았을 것이다. 이걸 잘 만드는건 또 다른 얘기지만 일단 컴포넌트 분리는 쉽게 할 수 있다. 그러면 다른 컴포넌트는 어떻게 나눠야할까?? 페이지에 디자인이 나와있고 디자인 컴포넌트를 제외하고 컴포넌트를 만드는 것은 프론트개발자 역할이다. 일단 데이터, 서버 통신은 생각하지 말자. 먼저 전반적인 디자인을 보고 공통되는 컴포넌트를 생각해볼수있다. 그리고 완전 독립적인 부분도 존재한다. (ex 헤더, 푸터) 조금 구체적으로 예를 들자면 아이템 리스트를 map으로 렌더링해줘야 하는 경우 보통 컴포넌트를 나눈다. 왜냐하면 아이템의 구체적인 행동이나 스타일은 상위 컴포넌트가 궁금하지 않기 때문이다. 이걸 객체지향의 추상화라고 볼 수도 있을 것 같다.

 

하나의 파일에 모든 정보가 나와있다면 그건 명령형 프로그래밍이다. 경우에 따라서 이해하기 쉬울수도 있지만 파일은 점점 커지게 되어 있고 결국 1000줄 10000줄이 넘는 유지보수가 불가능한 코드가 만들어지게 된다. 컴포넌트의 경우도 마찬가지다. 컴포넌트는 가벼워야한다. 그리고 조합해서 사용해야 한다. 다만 결합도와 응집도는 모두 잡을 수 없기 때문에 경우에 따라 적절히 나눠야한다. 이 적당히라는건 결국 역할과 책임을 나누고 추상화하는 능력에서 온다. 그리고 이건 객체지향과 연관이 되어있고 프론트개발자라고 할지라도 객체지향 공부를 꾸준히 해야한다는 것을 의미한다.(지극히 개인적인 의견)

 

이제 아래에서 조금 더 세부적으로 살펴보자.

컴포넌트에서 데이터를 다루는 방법

사실 위에서 전역적인 데이터나 api 처리는 전혀 얘기하지 않았다. 모든것을 한번에 얘기하면 오히려 하나도 이해하기 힘들어진다. 그래서 따로 빼서 얘기하려고 한다.

 

1. 비동기 처리

a. 직접 api 통신(fetch, axios)

가장 기본적인 방법은 직접 api 통신을 하는 것이다. 가장 기본적인 방법인만큼 그대로 사용한다면 문제가 조금 문제가 있다. 뷰에서 데이터 처리를 같이 하는 것, 로딩 에러처리를 일일히 처리해야되는 것, 캐싱 무한 스크롤등의 기능이 없는 것, url이 컴포넌트에 의존적인 것 등 많은 문제가 있다. api 모듈을 만들어서 관리한다던가 useAxios같은 훅을 만들어서 사용하는 등 여러가지 해결책이 있고 보통은 다른 라이브러리의 도움을 받는다.

 

b. 리덕스 미들웨어

가장 대표적인 데이터 처리 방법이다. thunk, saga같은 미들웨어를 이용해서 비동기처리를 한다. 서버 데이터와 클라이언트 데이터를 분리해야 한다는 의견도 있고 보일러 플레이트도 많아서 요즘은 다른 기술로 넘어가는 추세인것같다. 단 리덕스의 장점은 분명히 존재한다. 또한 rtk query도 나오고 리덕스도 변화를 따라가고 있으니 리덕스를 사용하는 것도 나쁜 선택은 아니라고 본다.

 

c. 리액트 쿼리, swr

최근에 가장 핫한 기술들이다. 리덕스에서 모든 처리를 하는건 너무 무겁고 직접 api 통신을 하는것도 좋은 방법은 아니기 때문에 이런 기술들이 나왔다. 직접 api 통신을 했을 때의 거의 모든 문제가 해결된다. 또한 프론트의 상태관리 라이브러리에 서버 데이터를 넣지 않는다는 장점도 있다. 카카오, 배민 같은 회사에서도 리덕스에서 리액트쿼리로 넘어가는 경우가 많이 보인다.

https://techblog.woowahan.com/6339/

https://tech.kakao.com/2022/06/13/react-query/

 

정리

어떤 방법, 기술을 선택하는 것도 중요하지만 본질을 봐야한다. 결국 본질은 역할분리다. 컴포넌트에서는 뷰의 역할에만 충실하면 된다. 다른 역할을 하면 컴포넌트가 비대해진다. 마치 백엔드에서 서비스 계층과 레포지토리 계층을 분리하는 것과 같다. 또한 복잡하지 않고 간단하게 코딩을 하는것도 중요하다. 리덕스 미들웨어로만 api 통신을 했을 때도 보일러플레이트때문에 생산성이 떨어지고 그냥 직접 api 통신을 하는 경우도 많이 봤다. 코드량이 적어야 좋은 코드는 아니지만 코드량이 적으면서 어렵지 않은 코드는 명백히 좋은 코드다. 이 점을 항상 생각하자.

2. 전역 상태관리

과거에는 자바스크립트 type에 모듈이 없었고 그냥 선언하면 전역변수로 관리가 되었다. 그래서 즉시실행함수 형태가 많이 사용되기도 했었는데 지금은 type module을 지원하기 때문에 다른 방법으로 전역변수를 관리한다.

 

전역 상태관리에 대해서는 깊게 얘기하지 않겠다. 간단하게는 react의 contextApi를 이용할 수 도 있고 조금 규모가 있으면 redux, mobx, recoil 등을 이용한다. 컴포넌트에서는 상태관리 라이브러리에서 가져다가 사용하고 저장하면 된다. 그리고 더 나아가면 커스텀 훅을 사용하기도 한다.

3. presentation component와 container component

리덕스를 사용해본 사람이라면 아마 한번씩 들어보고 적용해봤을 패턴이다. 리덕스 데이터나 dispatch하는 함수를 컨테이너 컴포넌트에서 프레젠테이션 컴포넌트로 전달한다. 이 패턴은 정말 많이 사용되었지만 현재는 추천되지 않는다. 물론 경우에 따라 필요할 수 도 있다. 최근에 이 패턴이 사용되지 않는 이유는 커스텀 훅 때문이다. 커스텀훅으로 컨테이너 역할을 대체할 수 있다. 대체할 수 있다면 번거롭게 컨테이너 컴포넌트를 만들필요가 없다. 단, 항상 절대적인 것은 없고 도입해야할필요가 있다면 도입해도 괜찮다. 결국 본질은 데이터 처리를 view와 분리해야 한다는 것이다. props로 전달을 받던지 커스텀 훅으로 전달을 받던지 어떤 방법으로든 말이다. 참고로 vac 패턴이라는 것도 존재한다. 꽤 흥미로운 패턴이라서 아래 링크를 달아두었다.

 

https://d2.naver.com/news/0568192

https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

4. 커스텀훅

사실 이 얘기를 가장 하고 싶었다. 컴포넌트를 잘 만들기 위해서는 커스텀훅을 잘 만들어야한다. 정말 전역적으로 사용하는 경우만 커스텀훅을 만드는게 아니다. 컴포넌트에서 데이터를 다루는 부분이 복잡해지게 되면 항상 커스텀훅으로 뺄 수 있는지 생각하자. 토스 한재엽님의 headless 컴포넌트글, 리액트 테트리스 구현, 우형의 마광휘님의 조언(친구가 들은 조언) 등을 통해 내가 얼마나 데이터 분리를 하지 않고 있었는지 깨달았다.

컴포넌트의 역할과 책임

컴포넌트에 대한 역할과 책임을 생각해보자. 

1.  내부 데이터

리액트는 state를 통해 데이터를 관리한다. 

2.  외부 데이터

리액트는 props를 통해 외부 데이터를 전달받는다. 이때 외부 데이터는 서버 데이터, 로컬 데이터, 쿠키 데이터, 상위 컴포넌트 데이터, 전역 상태 데이터 등 모든 값이 올 수 있다. 

3.  이벤트

컴포넌트에서는 이벤트를 직접 관리한다. 내부데이터 혹은 외부데이터를 다룬다.

4.  ui

가장 중요한 건 역시 ui다. 컴포넌트의 데이터(정적인 데이터 포함)를 렌더링해여 화면에 보여준다.

컴포넌트를 잘 만드는 방법

위에서 컴포넌트를 만드는 방법과 컴포넌트의 역할과 책임에 대해 공부했다. 이제 조금 더 심화과정을 살펴보자. 

1. 컴포넌트 어떻게 나눌것인가

컴포넌트를 어떻게 나눠야할지는 위에서 이미 얘기를 했다. 다만 좀 추상적이다. 그래서 조금 구체적으로 설명해보려고 한다.

a. 독립적인 컴포넌트 나누기

말솜씨가 없어서 직접 화면을 보고 설명해보겠다. 위는 티스토리 관리자페이지이다. 

가장 크게 나누면 header, sidebar, manage로 나눌 수 있다. header와 sidebar는 관리자페이지에서 공통적으로 사용되는 컴포넌트이고 가운데 부분인 manage 컴포넌트는 /mange 페이지에서만 사용되는 컴포넌트다.  

그리고 mange에서도 카드별로 컴포넌트를 분리할 수 있어보인다.

b. 데이터로 나누기

사실 독립적이라는 것은 좀 애매한 표현이다. 독립적이라고 항상 나누는 것이 옳은 것도 아니다. 너무 나누면 응집도가 떨어지고 너무 나누지 않으면 결합도가 떨어진다. 그리고 데이터가 포함되어 있지 않은 정적 컴포넌트라면 비교적 쉽지만 데이터가 포함되어 있다면 나누는 기준이 더 모호해진다. 일단 데이터를 기준으로 어떻게 나눌 수 있는지 살펴보자.

 

위 컴포넌트를 가볍게 구현해보았다. usePosts는 posts를 불러오는 훅이다. 그리고 suspense나 상세한 타입, 스타일은 생략했다.

const PopularPosts = () => {
  const { data, isLoading } = usePosts();
  return (
    <div>
      인기글
      <PopularPostList posts={data} isLoading={isLoading} />
    </div>
  );
};

const PopularPostList = ({ posts, isLoading }: { posts: { id: number; title: string }[]; isLoading: boolean }) => {
  const onClickBtn = () => {
    window.open(`/manage/statistics/entry/${id}`);
  };
  if (isLoading) return <div>로딩중...</div>;
  return (
    <ul>
      {posts.map(({ id, title }, index) => (
        <PopularPostListItem id={id} number={index + 1} title={title} onClickBtn={onClickBtn} />
      ))}
    </ul>
  );
};

const PopularPostListItem = ({ id, number, title, onClickBtn }) => {
  return (
    <li>
      <div>{number}</div>
      <div>{title}</div>
      <button onClick={onClickBtn}>통계</button>
    </li>
  );
};

 

컴포넌트 하나를 세 개로 나누었다. 잘 나눈걸까?? 데이터 관점에서 살펴보자. 일단 제일 상위 컴포넌트에서 api를 호출하고 PopularPostList에 data를 전달했다. 이렇게 할 필요가 있을까??

 

- PopularPostList에서 api를 호출하자

굳이 상위 컴포넌트에서 api를 호출하고 props를 전달할필요가 없어보인다. 지금같은 경우는 props를 내려주는게 2단계밖에 안되지만 실제 프로젝트에서는 조금만 신경쓰지 않으면 5,6단계까지 가는 경우도 있다. 상태관리 라이브러리를 사용한다던가 컴포넌트를 나누는 방법이 잘못되었을 수도 있지만 이런 기본적인 것만 신경써도 최소 1depth는 줄일 수 있다.

const PopularPosts = () => {
  return (
    <div>
      인기글
      <PopularPostList />
    </div>
  );
};

const PopularPostList = () => {
  const { data: posts, isLoading } = usePosts();
  const onClickBtn = () => {
    window.open(`/manage/statistics/entry/${id}`);
  };
  if (isLoading) return <div>로딩중...</div>;
  return (
    <ul>
      {posts.map(({ id, title }, index) => (
        <PopularPostListItem id={id} number={index + 1} title={title} onClickBtn={onClickBtn} />
      ))}
    </ul>
  );
};

const PopularPostListItem = ({ id, number, title, onClickBtn }) => {
  // 생략
};

c. 역할 책임으로 나누기

이제 좋은 코드가 되었을까?? PopularPostListItem이라는 컴포넌트는 number, title, button이 존재한다. 그리고 button event는 props로 전달받는다. PopularPostLIstItem의 역할과 책임은 무엇일까?? 인기글을 보여주는게 이 컴포넌트의 역할이다. 버튼의 역할까지 할 필요가 있을까?? 사실 지금의 경우에는 크게 상관이 없다. 만약 유지보수를 하지 않는다면 말이다. 하지만 높은 확률로 이 컴포넌트는 변경된다. 예를들어 버튼의 디자인과 클릭이벤트가 변경된다고 생각해보자. 그러면 PopularPostList의 onClickBtn 함수를 변경하고 PopularPostListItem의 button style을 변경해야 한다. 한개의 컴포넌트가 2개의 역할을 하기 때문에 유지보수할때 두개의 컴포넌트를 수정해야했다. 

 

- 버튼 자체를 상위 컴포넌트에 정의하자

이제 버튼의 스타일 혹은 이벤트가 변경될때 PopularPostListButton 컴포넌트만 수정하면 된다. 

const PopularPostListButton = ({ id }) => {
  const onClickBtn = () => {
    window.open(`/manage/statistics/entry/${id}`);
  };
  return <button onClick={onClickBtn}>통계</button>;
};

const PopularPostList = () => {
  const { data: posts, isLoading } = usePosts();
  if (isLoading) return <div>로딩중...</div>;
  return (
    <ul>
      {posts.map(({ id, title }, index) => (
        <PopularPostListItem id={id} number={index + 1} title={title} RightButton={<PopularPostListButton id={id} />} />
      ))}
    </ul>
  );
};

const PopularPostListItem = ({ id, number, title, RightButton }) => {
  return (
    <li>
      <div>{number}</div>
      <div>{title}</div>
      <RightButton />
    </li>
  );
};

2. 인터페이스

위에서 3가지 기법에 의해서 컴포넌트를 나눴다. 이제 좋은 컴포넌트가 되었을까?? 객관적으로 좋은 컴포넌트가 아니다. 재사용하기도 힘들어보이고 유지보수하기도 힘들어보인다. 예를들어서 비슷한 리스트아이템 컴포넌트를 구현해야하는데 숫자가 없는 경우라던가 오른쪽에 버튼이 아닌 이미지가 오는 경우, sub title이 오는 경우에 대처할 수 없다. 처음부터 뜯어고쳐야한다. 왜 이런일이 발생했을까?? 나는 독립적으로, 데이터로, 역할 책임으로 컴포넌트를 나눴는데 말이다. 그건 인터페이스를 고려하지 않았기 때문이다. 인터페이스의 정의를 찾아보면 다음과 같다.

 

인터페이스(interface)란 사물과 사물 사이 또는 사물과 인간 사이의 경계에서, 상호 간의 소통을 위해 만들어진 물리적 매개체나 프로토콜을 말한다. - http://wiki.hash.kr/index.php/%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4

 

중요한건 소통을 위해 만들어졌다는 것이다. 내가 위에 작성한 코드는 소통하고 있지 않다. 그냥 필요한 데이터를 인터페이스에 정의했을뿐이다. 그러면 컴포넌트의 인터페이스와 일반적인 인터페이스는 어떤것일까?? 부족한 내지식을 적기보다는 한재엽님의 글을 인용한다.

 

컴포넌트의 인터페이스

  1. 인터페이스는 사용하는 쪽을 위한 것이다.
  2. 컴포넌트를 사용하는 쪽에선 인터페이스를 보고 어떻게 동작할지 예상 가능해야 한다.

기본에 근거할수록 이해하기 쉽다. 웹의 문서는 HTML로 작성된다. 표준 인터페이스를 따라 인터페이스를 결정하면 의미를 파악하고자 하는 노력 없이 그 의미를 이해할 수 있다.

일반적인 인터페이스

‘일반적인’ 인터페이스란 구체적으로 다음 두 가지를 의미한다.

  1. 특정 도메인에 대한 맥락을 가지고 있지 않아서 어느 도메인에서든 사용할 수 있는 인터페이스
  2. 사용하는 입장에서 내부를 보지 않고도 이 컴포넌트의 역할을 이해할 수 있는 인터페이스

 

위의 글을 보고 한번 생각해보자. 왜 사용하는 쪽을 위한 것일까? 예를 들어 보겠다. 컴포넌트에 여러가지 옵션을 넣으면 제너럴하게 사용할 수있다는 장점이 있다. 그런데 사용하는 쪽에서는 여러가지 옵션을 모두 주는 것이 쉽지 않다. 결국 그 컴포넌트가 익숙한 사람이 아니라면 쉽게 사용할 수 없다. 익숙해지면 불편함을 느끼지 못한다. 하지만 좋은 코드는 익숙하지 않은 사람도 쉽게 이해할 수 있어야 한다. 그 중 가장 기본이 되는 것이 인터페이스다. 즉 사용되는쪽이 아니라 사용하는 쪽을 고려해야 한다.

그리고 이어서 예상 가능해야한다는 문장을 보자. 위에서 적은 설명과 같은 맥락이다. 설명은 조금 다를지몰라도 결국 좋은 코드, 좋은 인터페이스, 좋은 설계 등은 하나로 모이게 된다. 마치 전혀 관련없는 대수학 위상수학이 언젠가는 만나게되는것처럼 말이다.

 

이제 일반적인 인터페이스를 보자. 도메인에 대한 맥락을 가지고 있으면 안된다는 말을 코드스피츠 강의에서도 들은적이 있다. 도메인 네이티브 패턴. 최대한 도메인적인 요소는 분리해야한다고 말했다. 지금의 경우에는 도메인을 분리하는 것은 아니지만 맥락을 제거한다는 측면에서 유사하다. 그리고 내부를 보지 않고도 역할을 이해할 수 있어야 한다는 말은 추상화를 잘 해야된다는 뜻이다. 큰 회사일수록 변수명을 엄청 중요시한다. 변수명이 중요한 이유는 그 변수로 비지니스 로직이나 복잡한 로직을 감추고 함축해주기 때문이다. 인터페이스도 똑같다. 

 

이제 인터페이스를 생각해서 다시 리팩터링해보자.

const PopularPostListButton = ({ id }) => {
  const onClickBtn = () => {
    window.open(`/manage/statistics/entry/${id}`);
  };
  return <button onClick={onClickBtn}>통계</button>;
};

const PopularPostListChildren = ({ number, title }: { number: number; title: string }) => {
  return (
    <div>
      <div>{number}</div>
      {title}
    </div>
  );
};

const PopularPostList = () => {
  const { data: posts, isLoading } = usePosts();
  if (isLoading) return <div>로딩중...</div>;
  return (
    <ul>
      {posts.map(({ id, title }, index) => (
        <ListItem right={<PopularPostListButton id={id} />}>
          <PopularPostListChildren number={index + 1} title={title} />
        </ListItem>
      ))}
    </ul>
  );
};

interface Props extends LiHTMLAttributes<HTMLLIElement> {
  right?: React.ReactNode;
}

const ListItem: React.FC<Props> = ({ children, right, ...props }) => {
  return (
    <li {...props}>
      <div>{children}</div>
      {right}
    </li>
  );
};


1. 인터페이스에서 도메인적인 맥락을 제거

도메인적인 맥락을 모두 제거했다. number, title 같은 props는 계속 추가될 수 있다. 그래서 그냥 children으로 묶어버렸다. 또한 RightButton은 right로 변경했는데 버튼말고 다른 것들이 올 수 있기 때문이다. right라는 인터페이스를 보면 누구나 오른쪽에 값이 들어간다고 유추할 수 있다. (right라는 값은 컴포넌트 라이브러리에서도 자주 사용되고 나도 사용하는 인터페이스다. 그리고 재엽님의 블로그에도 right라는 네이밍을 사용한다고 적혀있다.)

2. required => optional

오른쪽에 노드는 오지 않는 경우도 있다. 그렇기 때문에 optional로 변경했다.

3. Props extends LiHTMLAttributes<HTMLLIElement>

컴포넌트에 className이나 onClick 등등 많은 props를 전달해줄 수 있다. 그때마다 일일히 추가해주는 일은 너무 번거롭다. 또한 인터페이스가 비대해진다. 이럴때는 상속을 사용하자

 

생각해보기

1. 응집도와 결합도

ListItem에는 실질적으로 right라는 인터페이스만 존재한다. 그렇기 때문에 유지보수할 때 컴포넌트를 변경할일이 적다. 즉 결합도가 줄었다. 그런데 동시에 응집도도 줄었다는 생각이 든다. 결합도와 응집도는 적절한 밸런스를 유지해야한다. 지금은 너무 결합도를 줄이는데 신경을 썼다는 생각이 들지 않는가?? 예를들어서 number라 들어간 ListItem이 다수 존재한다고 생각해보자. 그 때마다 다시 number가 들어간 children 컴포넌트를 만드는건 너무 비효율적이다. 그러면 다시 number를 넣어야할까?? 그러면 ListItem의 두번째줄에 회색글씨의 글자가 들어간다고 생각해보자. 그러면 이때도 props를 추가해야한다. 그리고 두번째 줄에 관리자 · 10명 이런 형태가 온다고 생각해보자. 이때도 옵션을 추가해줘야할까?? 여기서 끝나면 다행이지만 끊임없이 디자인이 변경된다. 또한 디자인이 변경될때마다 신경써서 컴포넌트를 리팩터링할 수도 없다. 이럴 때 사용할 수있는건 새로운 컴포넌트를 만드는 것이다. 자주 사용되는 컴포넌트를 만들고 그 컴포넌트를 children으로 넘기면 된다. 단, 이런 컴포넌트도 많아지면 관리가 안된다. 그래도 괴물같이 거대한 하나의 컴포넌트를 만드는것보다는 낫다. 사실 잘 모르겠다. 컴포넌트를 만드는건 너무 어렵다. 한때 컴포넌트만 잘만드면 끝인가? 프론트엔드 개발자는 잘해봤자 거기서 거긴가? 라고 생각했었는데 나는 그런 생각을 하기 10년은 이르다는 것을 깨달아버렸다...

2. 컴포넌트 분리

PopularPostList라는 컴포넌트만 만들고 ListItem에 data를 전달해주면 되는데 지금은 추가적인 2개의 컴포넌트를 더 만들었다. 데이터, 역할 책임을 분리했지만 다른 관점에서 보면 불필요한 컴포넌트를 2개나 더 만들었다. 추상화했지만 이게 필요한 추상화인지 생각해볼 필요가 있다. 아무리 인터페이스를 잘 만들어도 결국엔 의존관계가 만들어지게 된다. (독립적인 컴포넌트라고 할지라도 무작정 나누는 것이 좋은 것은 아니다.) 추상화할필요없는 간단한 컴포넌트라면 오히려 추상화를 통해서 이해하기 힘들어질지도 모른다. 그 적절한 선이라는 걸 찾아야한다. 또한 아무리 확장성을 생각해도 상황에 따라 코드는 달라져야 한다. 아래 영상에서 그때는 맞았지만 지금은 틀려서 리팩터링해야 한다고 말한다. 즉 잘 만들어진 컴포넌트라고 할지라도 상황에 따라 변경해야한다는 것을 인지해야 한다. 그런데 대부분 회사에서는 리팩터링할 시간을 주지 않는다. 이러한 상황속에서 어떻게 해야할지는 나도 잘 모르겠다. 그나마 할 수 있는 건 컴포넌트를 만드는 것. 그리고 클린 코드를 신경쓰고 조금씩 조금씩 필요하다면 리팩터링 하는것 정도가 있을 것 같다. 

3. 테스트코드(TDD)

2번에 이어서 얘기 하겠다. 테스트 코드를 짠다면 조금 더 편하게 리팩터링 할 수있다. 하지만 프론트엔드에서 테스트는 시간도 오래 걸리고 효율적인가에 대해서 조금 의견이 분분하다. 부정적 => 긍정적 => 반반... 으로 가고있다. 지금은 컴포넌트 얘기를 하고있었는데 리팩터링으로 빠지고 있다. 

다시.. 컴포넌트를 만들때 TDD로 만든다고 생각해보자. 미리 인터페이스도 정해놓고 test case도 정해놓으면 확실히 컴포넌트를 확실하게 만들 수 있다. 테스트코드는 내가 깊게 알지도 못하고 주제와는 약간 다르다고 생각해서 이정도만 얘기하겠다. 한마디만 덧붙이자면 테스트코드를 짠다고 코드 퀄리티 혹은 좋은 컴포넌트가 만들어지지는 않는다. 테스트 케이스는 결국 작성하는 개발자의 능력에 달려있고 100개의 테스트 케이스를 작성해도 잘못된 경우가 있으면 그건 좋은 테스트가 아니다. 다만 테스트코드를 작성하다보면 내가 코드를 얼마나 잘못 작성했는지 알 수 있다. 특히 의존성이나 비대해지는 것들을 막을 수 있다. 

 

2편 보러가기

https://ms3864.tistory.com/464

 

컴포넌트를 잘 만드는 방법 2편(리액트)

배경 약 1년 반 전 컴포넌트를 잘 만드는 방법(리액트) 글을 썼다. 우연히 출퇴근길 개발 읽기에 올라갔고 많은 사람들이 내가 쓴 글을 읽었다. 1년 반이 지난 지금 나는 컴포넌트에 대해서 얼마

ms3864.tistory.com

 

참고한 글

https://www.youtube.com/watch?v=edWbHp_k_9Y&ab_channel=%ED%86%A0%EC%8A%A4
https://jbee.io/web/components-should-be-flexible/
https://jbee.io/react/headless-concept/

728x90
LIST