우당탕탕 Next.js 개발기 - ④ 퍼널 구조에서 관심사를 분리하며 컴포넌트 설계 해보기

김동현·2024년 7월 31일
6
post-thumbnail

이번 포스팅은 컴포넌트 설계와 관련된 이야기입니다.
현재 프로젝트에서 프론트엔드 부분을 혼자 맡으며 고군분투하다 보니 고민한 내용이 충분히 모자란 내용일 수 있습니다. 그럼에도 불구하고 제 고민을 공유하며 비슷한 고민을 하는 프론트엔드 개발자분들에게 조금이라도 도움이 되기를 기원합니다.

퍼널 구조를 소개합니다.

우선 제가 작성한 이 게시물에서도 밝혔지만 오늘 관심사 분리를 진행할 구조는 바로 퍼널 구조입니다.

퍼널은 마케팅 용어로 시작하여 토스 팀에서 재정의 한 구조인데요. 여러 페이지들을 통해 상태를 수집하고 결과 페이지를 보여주는 형태입니다. 오늘 제가 소개할 구조도 정확히 퍼널 구조입니다. 스크린샷을 통해 확인해볼까요?

위의 스크린샷에서 볼 수 있듯이 퍼널은 총 4단계로 이루어집니다.

  1. Page : 무언가를 누르면 모달을 호출할 수 있습니다.
  2. A_State : form 구조를 가지며 form 의 내용을 서버에게 보내야 합니다.
  3. B_State : form 을 보내고 나서 loading 을 맡습니다.
  4. C_State : form 의 response 를 맡습니다. 최종 도착지입니다!

이러한 구조를 가진 퍼널에 대해서 관심사를 분리하여 가독성이 높은 코드를 작성해 보겠습니다.

관심사를 분리한다는 것은?

관심사를 분리한다는 것은 무엇을 의미할까요? 한 번에 한 가지 일만 처리할 수로 있도록 나누는 것을 의미합니다. 코드가 단위 별로 하나의 관심사에 충실할 수 있도록 만드는 것 입니다.

만약 관심사 분리가 이루어지지 않았다면 어떨까요? 하나의 컴포넌트안에 모든 로직을 때려박으니 엄청나게 긴 컴포넌트가 탄생합니다. 이는 분명히 좋지 못한 패턴으로 가독성과 유지보수성에서 모두 안좋은 영향을 미칩니다.

그에 반해 관심사를 분리하게 되면 어떤 좋은점들이 있을까요?

  1. 개별 컴포넌트의 복제가 없어지고 목적이 단일화 됩니다. 이는 곧 컴포넌트가 강한 응집도를 가졌다는 말로 해석될 수 있습니다.
  2. 응집력 높은 단일 책임은 자연스러운 확장가능성들을 낳게 됩니다.이는 실전적 예제에서 피부에 와닿게 설명해보겠습니다.
  3. 각각의 코드는 낮은 결합도를 가지게 됩니다. 이는 서로 다른 컨텍스트에서도 더 쉽게 사용될 수 있게 만듭니다.

말로만 해서는 정확히 와닿지를 않습니다. 한번 실질적인 예제를 통해서 이를 분리해보겠습니다.

우선 관심사를 전혀 분리하지 않은 코드를 먼저 보고 이를 해결할 수 있는 방법에 대해 알아보겠습니다.

const PageComponent = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [form, setForm] = useState();

  const onSubmitForm = async () => {
    await axios.post(url, { form });
  };

  return (
    <>
      <Button onClick={() => setIsOpen(true)}>모달 열기</Button>
      <Modal open={isOpen} setForm={setForm} onSubmitForm={onSubmitForm} />
    </>
  );
};

우선 PageComponent 입니다. 전혀 관심사 분리가 안된 모습입니다. Modal 의 상태도 관리하고, loading 상태도 관리하고, form 상태도 관리하고 있습니다. 최악의 모습입니다.

두번째로는 Modal 내에서 컴포넌트들입니다. 스크린샷은 토스의 개발 컨퍼런스중 한 장면인데요 위와 같이 설계하고 코드가 작성되어 있다면 어떤가요? 관심사가 비즈니스 로직에 의해 잘 분리된 것 같나요? 다음과 같은 문제점이 있다고 토스팀은 지적합니다.
1. 흩어져 있는 페이지 흐름 : 중간에 퍼널이 하나라도 늘어나게 된다면 코드를 왔다갔다 하면서 유지보수를 해야합니다.
2. 흩어져 있는 상태 : API 에 대한 기능을 추적하고 싶다면 상태를 사용하고 있는 집주소 뿐만 아니라 가입방식과 주민번호에 대해서도 추적을 해야합니다.

이러한 부분들을 이제 실전적 예제에서 어떻게 풀어나가는지 확인을 해봅시다.

실전적 예제 ① - Page 과 Modal 간의 관심사 분리 : 선언적 프로그래밍

가장 먼저 Page 와 Modal 의 관심사를 분리해보겠습니다. 각각의 관심사가 무엇일까요?

  1. Page : 모달을 선언하는 곳으로 한정짓는게 좋아보입니다. 페이지에서는 굳이 모달 내부의 상태를 알 필요가 없습니다. 모달이 할 일은 모달이 할 수 있도록 놔둡시다.
  2. Modal : State_A ~ State_C 를 관리합니다.

따라서 이를 새롭게 코드를 짜면 다음과 같습니다. 아주 깔끔해졌고, Page 는 오롯이 Modal 을 선언적 프로그래밍 형식으로 선언하는 관심사만 가지게 됐습니다. 앞으로도 다른 Page 위계를 가진 컴포넌트도 이렇게 작성한다면 더욱 코드 관리가 쉬워질것 같네요.

const PageComponent = () => {
  const {openModal} = useModal('Dialog');

  return (
    <>
      <Button onClick={openModal}>모달 열기</Button>
    </>
  );
};

useModal 과 Modal 선언이 어떻게 되는지 궁금하신 분들은 제 글인 추상화 수준을 맞춘 클린한 Modal 개발하기 를 봐주세요!

실전적 예제 ② - Modal 내에서 관심사 분리 : 상태 뭉치기

두 번째는 상태를 뭉쳐서 관심사를 분리하는 것인데요. 어떻게 하는 걸까요?

  1. Modal : State_A 부터 State_C 에 이르는 비즈니스 로직을 담당합니다.
  2. State_A~ State_C 컴포넌트 : 오직 UI 로직만을 담당합니다.

그러면 실제로 모달 컴포넌트의 코드를 볼까요?

const Modal = ({isOpen, onClose} : Props) => {
  const [step, setStep] = useState<'state_a' | 'state_b' | 'state_c'>('state_a');
  
  /* 중략 */
  

  return (
    <Modal open={isOpen} onClose={onClose}>
      {step==='state_a' && (<StateAComponent {...stateAProps}/>)}
	  {step==='state_b' && (<StateBComponent {...stateBProps}/>)
      {step==='state_c' && (<StateBComponent {...stateCProps}/>)}
    </Modal>
  );
};

이제 모든 비즈니스 로직과 관련된 관심사는 Modal 에만 집중되어 있습니다. 파일을 넘나들면서 유지보수를 할 필요가 없고, 새로운 퍼널이 필요할때 설계가 훨씬 간편해졌습니다.

실전적 예제 ③ - 각 Step 별로 관심사 분리 : custom hook

위의 예제에서 /*중략*/ 이라고 쓴 부분이 불길하셔야 합니다. 왜냐하면 저기에 비즈니스 로직을 다 때려박으면 문제가 크게 생기거든요. 컴포넌트로는 응집도가 높을지 모르나, 코드로서는 응집도가 매우 떨어집니다. 저는 이 문제를 custom hook 으로 해결해보았습니다. 다음과 같이요.

const Modal = ({isOpen, onClose} : Props) => {
  const [step, setStep] = useState<'state_a' | 'state_b' | 'state_c'>('state_a');
  
  const {value, onChange, errors, isDiableButton, handleSubmit} = useChargeForm();
  const {mutate, isPending} = useChargeMutation();
  
  const stateAProps = {
  	value,
    onChange,
    errors,
    isDisableButton,
    onClickButton : ()=>{
    	handleSubmit(mutate)();
      	setStep('state_b');
    }
  }
  
  /* stateB 및 stateC 생략*/
  
  

  return (
    <Modal open={isOpen} onClose={onClose}>
      {step==='state_a' && (<StateAComponent {...stateAProps}/>)}
	  {step==='state_b' && (<StateBComponent {...stateBProps}/>)
      {step==='state_c' && (<StateBComponent {...stateCProps}/>)}
    </Modal>
  );
};

이제는 훨씬 관심사가 분리되어 응집도가 높아졌습니다.

  1. 각각의 비즈니스 로직을 hook 으로 묶어냈습니다. Modal 입장에서는 선언적으로 비즈니스 로직을 호출하는 샘입니다.
  2. 컴포넌트에서 필요한 props 합성은 custom hooks 에서 return 한 결과물들로 합성을 합니다. 따라서 하나의 객체에 묶게 될 수 있으며, 훨씬 응집도가 높아졌습니다.

Reference

profile
Frontend Developer

0개의 댓글