컴포넌트 추상화와 아키텍처 설계

xxziiko·2024년 1월 5일
1

[React]

목록 보기
5/5
post-thumbnail

회사에서 기존 레거시 코드의 마이그레이션 작업을 준비하기 시작했고, 초기 프로젝트 구축 및 설계를 담당하게 되었다. 이를 계기로 컴포넌트 추상화에 대해 정의하고 프로젝트 구조를 결정하는 과정과 간단한 회고를 기록하고자 한다.



🤔 컴포넌트 추상화?

React는 컴포넌트 기반 라이브러리이다.
컴포넌트 단위로 개발 함으로써 얻을 수 있는 이점은 재사용성, 관심사의 분리, 응집도 있는 로직등이 있다. 이는 컴포넌트를 유연하게 만든다. 프론트엔드 개발은 개발 패러다임이 자주 바뀌기 때문에 코드 폐기 또는 수정에 유연하게 대처할 수 있어야 한다. 컴포넌트를 유연하게 만들기 위해서는 다양한 요소들을 적절하게 나누고 추상화를 해야한다.

SOLID 원칙

객체지향 프로그래밍에서 나온 개념이다. 그 중 첫번째 원칙인 SRP(단일 책임의 원칙: Single Responsibility Principle)에 따라 하나의 컴포넌트는 하나의 책임만을 가져야 한다. 이 원리를 적용하면 책임 영역이 명확하기 때문에 동작의 연쇄작용에 자유로울 수 있다.




신규 프로젝트의 컴포넌트 조건을 먼저 정의해보았다.
  • 컴포넌트에는 조건문이 들어가면 안된다. → 조건문이 들어가는 특정 도메인에 종속될 수 있는 여지가 생긴다.
  • 컴포넌트가 수정되어야 한다면 수정사항이 사용되는 모든 부분에 공통적으로 해당해야 한다.
  • 하나의 컴포넌트는 하나의 책임만 갖는다.
  • 컴포넌트에는 비즈니스 로직이 없어야 한다.

초기 회의에서 의견이 나왔던 구조는 atomic pattern이었다.

/src
	ㄴcomponents
			ㄴatoms
			ㄴmolecule
			ㄴorganisms
			ㄴlayouts
	ㄴpages
	...
  • atom
    • button(lined, no-lined, contain..)
    • Input(no-lined, lined …)
    • table
    • tableHeader
    • tableFooter
    • tableRow
    • tableCell
  • molecule (UI atom의 묶음 단위)
    • selectbox (input+dropdown)
    • datepickers
  • organisms (비즈니스 데이터가 포함되는 단위)
    • __table.tsx 예시 : accountingTable.tsx (tableHeader + accountingTableRow+ tableFooter)
  • layouts (template)


초안으로 생각했던 프로젝트 구조인데, 다음과 같은 단점을 가지고 있었다.

  • 비즈니스단위별로 컴포넌트를 찾는 것이 어려움
  • 폴더구조는 초기에만 접하게 되는데, 이러한 구조는 소스코드에 대한 이해가 없는 상태에서 오히려 더 파악이 어려울 수 있음.


그리고 atomic pattern를 적용하기 위해 기존 소스코드를 분류해보니organism의 중복과 확장에 대한 고민과 organismtemplate 모호함이 문제점이었다. 기존 소스코드에 atomic pattern이 부합하지 않았다.

두번째 아키텍처 후보는 compound-component pattern이었다.
기존 개발방식은 props가 많아질수록 복잡해지고 확장하기 어려워지는 구조였다. view에 대한 상태 값과 logic에 대한 상태값의 구분이 어려워 가독성이 좋지 않았고 props가 많아질수록 디버깅이 어렵고 변경된 요구사항에 유연하지 못하다고 판단했기 때문에 해당 단점을 보완하고 추상화에 용이한 compound-component pattern을 도입하자고 의견을 냈다.




⚙️ 아키텍처

이런저런 회의 끝에 SOLID 원칙을 유념하여 프로젝트 아키텍처를 결정했다. 새로운 프로젝트의 아키텍처는 compound-component pattern(UI) + custom hook(with React-query) 으로 구성하기로 했다.


compound-component pattern 이란?

  • 하나의 컴포넌트를 여러가지 집합체로 분리한 뒤, 각 컴포넌트를 사용하는 쪽에서 조합하여 사용하는 컴포넌트 패턴
  • prop driling을 제거할 수 있고 표현적(expressive) 이고 선언적인(declarative) 컴포넌트를 만들 수 있다
  • 부모와 자식간의 컴포넌트 상태를 공유할 때 context api를 사용한다.(context api의 가장 좋은 예시라고 한다.)

장점

  • 유연한 마크업 구조
    • 컴포넌트 UI가 뛰어난 유연성을 가지고 있어 하나의 컴포넌트로부터 다양한 케이스를 생성할 수 있음
    • sub Component들의 순서를 변경하기 용이하고 필요한것만 사용할 수 있음
  • API 복잡성 감소: 각 prop는 가장 적합한 sub Component에 연결되어 있는 형태
  • 관심사 분리: 대부분의 로직은 기본 컴포넌트에 포함되며, React.Context는 모든 자식 컴포넌트의 statehandler를 공유하는 것에 사용됨

단점

  • 높은 UI 유연성
    • 예기치 않은 동작을 유발할 가능성이 있음
    • 필요없는 자식컴포넌트의 존재
  • 오히려 코드량이 많아질 수 있음 : JSX 행 수가 증가


사용될 컴포넌트의 종류는 두가지로 정의했다.

  • 범용적인 컴포넌트 (ex. button, input, selectbox)
  • 특정 도메인에 종속된 비즈니스 컴포넌트

✅ 범용적인 컴포넌트

  • button, input 등과 같은 모듈 단위의 컴포넌트
  • 비즈니스 로직이 들어가서는 안된다.
  • 특정 도메인에만 필요한 prop을 추가하지 않는다.
  • 특정 도메인에만 필요한 분기처리를 하지 않는다.
// button.tsx

export default function Button(props: IButton) {
  const { command, style, width = 64, height = 40,  onClick } = props;

  return (
    <Button width={width} height={height} onClick={() => onClick()} style={style}>
      {command}
    </Button>
  );
}

✅ 특정 도메인에 종속된 비즈니스 컴포넌트

  • modal, card와 같이 재사용성이 있으나 도메인으로 인해 범용적으로 사용하기 어려운 컴포넌트 (?)
  • compound-component pattern(합성 컴포넌트) 디자인 패턴을 도입하여 도메인과 컴포넌트의 책임을 세분화 함으로써 의존성을 낮춘다.
// modal.tsx

const ModalContext = createContext<any | null>(null);

export const Modal = (props: IModalProps) => {
  const { children, ... } = props;
  const value = {};

  return (
    <ModalContext.Provider value={value}>
      <Container>
        <Background>
          <ModalLayout ... >
            <Header>
              <Title>{title}</Title>
              <SubTitle>{subTitle}</SubTitle>
            </Header>
            <Content>{children}</Content>
          </ModalLayout>
        </Background>
      </Container>
    </ModalContext.Provider>
  );
};

const InputForm = (props: IInputProps) => {
  const { ... } = props;

  const renderingComponent = () => {
    switch (type) {
      case 'select':
        return (
          <SelectBox
           ...
          />
        );
      case 'input':
        return (
          <Input
           ...
          />
        );

      default:
        break;
    }
  };

  return <>{renderingComponent()}</>;
};

const SingleButtonFooter = (props: ISingleButtonFooter) => {
  const { ... } = props;
  return (
    <SingleButtonLayout>
      <Button ... />
    </SingleButtonLayout>
  );
};

Modal.InputForm = InputForm;
Modal.SingleButtonFooter = SingleButtonFooter;
...


✅ custom hook

  • 기능 별custom hook을 만들어 비즈니스 로직을 추상화 한다.
  • 훅을 통한 책임의 분리는 컴포넌트의 응집도를 높이고 도메인과의 결합을 낮춘다.

예시) 데이터를 가져와서 페이지에 쓰이는 데이터로 가공하고 mutate 하는 custom hook

import { useEffect, useState } from 'react';
import useModalQuery from '@/useModalQuery';

export default function useModal({ id }: { id: number }) {
  const [data, setData] = useState<any>({});
  const {
    data,
    isLoading,
    mutateData,
  } = useModalQuery({ id });

  useEffect(() => {
   /* data 가공 */
  }, [data, isLoading]);

  return {
    data,
	mutateData
  };
}
  • hook을 설계할 때 가장 유의해야 할 점은 하나의 hook이 어떤 역할을 할 것인지, 중요한 역할이 무엇인지 명확하게 드러나게 설계해야한다.
  • 복잡한 함수는 custom hook 안에 숨기고, hook의 중요한 역할을 드러내어 선언형 프로그래밍으로 작성해야 한다.


선언형 프로그래밍

  • 핵심 데이터만 전달받고 세부 구현은 뭉쳐 숨겨 두는 개발 스타일
  • ‘무엇’을 하는 함수인지 빠르게 인지 가능
  • 세부 구현은 내부에 뭉쳐둠
  • ‘무엇’만 바꿔서 쉽게 재사용 가능
  • 함수가 어떤 방식으로 동작하는지보다 어떤 결과를 나타내는지에 중점


✍🏼 회고

글을 정리하면서 custom hook에 대한 공부가 많이 되었다.
사실 기존에도 custom hook을 사용하고 있었는데, 선언형 프로그래밍을 접하고 나서 hook에 대한 이해도가 낮은 상태로 사용하고 있었다는 것을 깨달았다. 그 전에는 비즈니스로직과 UI 분리에만 초점을 두고 custom hook에 모든 로직을 안보이게 넣어놓고 클린하다고 생각했는데 선언형 프로그래밍을 접하고 나서 얻어맞은 기분이었다.

클린 코드는 짧은 코드가 아닌 읽기 좋은(찾고 싶은 로직을 빠르게 찾을 수 있는) 코드입니다.

가능한 모든 코드를 뭉쳐서 하나의 커스텀 훅으로 묶어 배치하는 것이 좋은 것이 아니라, 해당 컴포넌트가 어떤 데이터를 어떻게 렌더링 하고 있는지 파악을 할 수 있도록 작성하는것이 더 좋은 방법입니다. 어떻게 동작하는지 알 필요가 없는 부분만 커스텀 훅으로 숨겨두는 것입니다.

프로젝트 아키텍처를 결정하고 나서 신규 페이지 작업을 테스트 삼아 적용해볼 수 있는 기회가 생겨 적용해 보았는데, 선언형 프로그래밍으로 하나의 기능만 하는 custom hook을 만드는 것이 예상보다 많은 것을 생각하게 했다. 과연 선언적으로 잘 작성 한 것일까? 다른 사람이 봤을 때 어떤 기능을 하는지 예상이 될까? 에 대한 고민을 많이 하면서 계속 리팩토링 했다. 지속적으로 리팩토링하면서 custom hook에 대한 포스팅을 한번 더 하면 좋을 것 같다.

compound-component pattern에 대해서도 느낀점이 있다.
아직 서너개의 컴포넌트 정도 만들어본 정도라 단점이라고 언급했던 많은 코드량은 체감하지 못했다. 그 전에는 컴포넌트의 추상화에 대해 어려움이 많아 유지보수 하다보면 비즈니스 로직과 결합도가 높아져 답답했었는데, 확실히 컴포넌트를 더 세분화 함으로써 의존도를 낮출 수 있었다.

아키텍처와 추상화에 대해 고찰하게 되면서 프론트엔드 아키텍처의 변천사(?)를 접하게 되었는데, 조만간 따로 정리해보면 좋겠다.

아무튼 compound-component pattern와 custom hook의 조합.. 꽤나 강력한 것 같다.




참고
추상화에 대해 여러 글을 접하던 중에, 많이 공감이 가고 인사이트를 얻었던 글
https://www.nextree.co.kr/p6960/
https://fe-developers.kakaoent.com/2022/221020-component-abstraction/
https://blog.leehov.in/57
https://daily-programmers-diary.tistory.com/8
[리액트 디자인 패턴] Compound Component Pattern (합성 컴포넌트 패턴) 알아보기

컴파운드 컴포넌트 패턴으로 만들기 리액트 디자인 패턴 2가지

profile
코딩하는 감자 🥔

0개의 댓글