회사에서 기존 레거시 코드의 마이그레이션 작업을 준비하기 시작했고, 초기 프로젝트 구축 및 설계를 담당하게 되었다. 이를 계기로 컴포넌트 추상화에 대해 정의하고 프로젝트 구조를 결정하는 과정과 간단한 회고를 기록하고자 한다.
React는 컴포넌트 기반 라이브러리이다.
컴포넌트 단위로 개발 함으로써 얻을 수 있는 이점은 재사용성, 관심사의 분리, 응집도 있는 로직등이 있다. 이는 컴포넌트를 유연하게 만든다. 프론트엔드 개발은 개발 패러다임이 자주 바뀌기 때문에 코드 폐기 또는 수정에 유연하게 대처할 수 있어야 한다. 컴포넌트를 유연하게 만들기 위해서는 다양한 요소들을 적절하게 나누고 추상화를 해야한다.
객체지향 프로그래밍에서 나온 개념이다. 그 중 첫번째 원칙인 SRP(단일 책임의 원칙: Single Responsibility Principle)에 따라 하나의 컴포넌트는 하나의 책임만을 가져야 한다. 이 원리를 적용하면 책임 영역이 명확하기 때문에 동작의 연쇄작용에 자유로울 수 있다.
초기 회의에서 의견이 나왔던 구조는 atomic pattern이었다.
/src
ㄴcomponents
ㄴatoms
ㄴmolecule
ㄴorganisms
ㄴlayouts
ㄴpages
...
button
(lined, no-lined, contain..)Input
(no-lined, lined …)table
tableHeader
tableFooter
tableRow
tableCell
selectbox
(input+dropdown)datepickers
__table.tsx
예시 : accountingTable.tsx
(tableHeader + accountingTableRow+ tableFooter)초안으로 생각했던 프로젝트 구조인데, 다음과 같은 단점을 가지고 있었다.
그리고 atomic pattern를 적용하기 위해 기존 소스코드를 분류해보니organism
의 중복과 확장에 대한 고민과 organism
과 template
모호함이 문제점이었다. 기존 소스코드에 atomic pattern이 부합하지 않았다.
두번째 아키텍처 후보는 compound-component pattern이었다.
기존 개발방식은 props
가 많아질수록 복잡해지고 확장하기 어려워지는 구조였다. view
에 대한 상태 값과 logic
에 대한 상태값의 구분이 어려워 가독성이 좋지 않았고 props
가 많아질수록 디버깅이 어렵고 변경된 요구사항에 유연하지 못하다고 판단했기 때문에 해당 단점을 보완하고 추상화에 용이한 compound-component pattern을 도입하자고 의견을 냈다.
이런저런 회의 끝에 SOLID 원칙을 유념하여 프로젝트 아키텍처를 결정했다. 새로운 프로젝트의 아키텍처는 compound-component pattern(UI) + custom hook(with React-query) 으로 구성하기로 했다.
prop driling
을 제거할 수 있고 표현적(expressive) 이고 선언적인(declarative) 컴포넌트를 만들 수 있다context api
를 사용한다.(context api
의 가장 좋은 예시라고 한다.)prop
는 가장 적합한 sub Component에 연결되어 있는 형태React.Context
는 모든 자식 컴포넌트의 state
와 handler
를 공유하는 것에 사용됨사용될 컴포넌트의 종류는 두가지로 정의했다.
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
와 같이 재사용성이 있으나 도메인으로 인해 범용적으로 사용하기 어려운 컴포넌트 (?)// 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
을 만들어 비즈니스 로직을 추상화 한다. 예시) 데이터를 가져와서 페이지에 쓰이는 데이터로 가공하고 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 (합성 컴포넌트 패턴) 알아보기