복잡한 페이지 이동, 한 눈에 파악할 수 있게 설계하기

GY·2023년 10월 18일
2
post-thumbnail

개발을 하다보면 추상화 등 보다 좋은 컴포넌트 설계에 대한 고민을 반드시 하게 됩니다.

저 역시 마찬가지였는데, 그 중 한 케이스가 회원가입, 로그인 및 계정 연동 기능 구현을 맡았을 때였습니다.
메타마스크, 카카오톡, 구글 등 많은 간편로그인 기능을 제공하면서도 이메일/패스워드로 가입한 기존 계정과 간편로그인 계정, 그리고 메타마스크와 sns 로그인 계정을 연동하는 플로우를 구현해야 했습니다. 이에 따른 예외 케이스 - 이미 계정을 연동했거나, 다른 계정을 연동해야 하는 경우 등에 대한 플로우도 고려해야 했죠.

그러면서 들었던 고민이 있었습니다.

  • 많은 뎁스를 거치다 보니 각 페이지 이동의 흐름이 보이지 않음
    • 각 컴포넌트가 어디로 이동하도록 만드는지 확인해야함
    • 특히 리팩토링할 때 어려움을 겪음
  • 상탯값이 흩어진 컴포넌트에 각각 관리되고 있음
    • 모두 리덕스로 전역 상탯값으로 관리할 필요는 없는데...

오늘은 이런 경우 어떻게 컴포넌트 구조를 개선할 수 있는지에 대해 정리하려 합니다.

(사내 코드는 공개할 수 없으니 마침 진행중인 프로젝트에 비슷한 형태로 구현해보겠습니다.)

토스에서 이러한 고민을 해소할 수 있는 세션이 공개된 적이 있습니다.
토스의 slash 23 특별 세션에서 공개되었던 useFunnel을 참고해 적용해보고, 이 과정을 정리했습니다.



적용해보기

오늘 적용해볼 부분은 이 부분입니다.

칸반보드 프로젝트에서 사이드바의 메뉴를 다루는 부분입니다.

많은 메뉴들이 있는데 그 중 change background를 예로 들면, 눌렀을 때 사진, 색상, 사용자 지정 이미지 카테고리를 선택할 수 있고 각각을 누르면 선택할 수 있는 화면이 나옵니다.

즉 사이드 바에는 각각의 메뉴들이 있고, 각 메뉴를 누르면 그에 맞는 뎁스별 컴포넌트가 표시됩니다. 그만큼 사이드바에서 표시할 컴포넌트의 갯수는 많아지겠죠!

위에서 든 예시를 가지고 구현 로직을 먼저 작성해보겠습니다.


일반적인 경우 문제점

먼저 쉽게 생각할 수 있는 일반적인 방식으로 코드를 작성해보았습니다.


function Drawer() {
  const [searchParams] = useSearchParams();
  const content = searchParams.get(SEARCH_PARAMS_KEY.MENU) 

  const contents = {
    [DRAWER_CONTENT.CHANGE_BACKGROUND]: <ChangeBackground />,
    [DRAWER_CONTENT.PHOTOS]: <BackgroundPhotos />,
     
//...
      
    <div>
      {contents[content]}
    </div>
  };

Drawer 컴포넌트 안에 표시할 컴포넌트를 생성한 다음 쿼리파라미터별로 표시할 컴포넌트들을 객체로 정의해주었습니다.
그리고 현재 쿼리 파라미터로 이 객체에 접근해 컴포넌트를 렌더링해줍니다.


이러한 구조로 코드를 작성하면 다음과 같은 단점이 있습니다.


시각화 측면에서의 단점: 페이지 이동의 흐름이 보이지 않음

위에서 예시로 든 이미지를 보면, 사이드바에는 많은 메뉴들이 있습니다. 이 중 여러 메뉴들이 각각 클릭하면 위와 같이 여러 뎁스를 거쳐 컴포넌트를 보여주게 됩니다. 즉, 여러 유저 플로우가 존재합니다.

그럴 경우 한 객체 안에 모든 사이드 바안에 렌더링 될 컴포넌트들을 정의해주게 됩니다.


function Drawer() {
  const [searchParams] = useSearchParams();
  const content = searchParams.get(SEARCH_PARAMS_KEY.MENU) 

  const contents = {
    [DRAWER_CONTENT.CHANGE_BACKGROUND]: <ChangeBackground />,
    [DRAWER_CONTENT.PHOTOS]: <BackgroundPhotos />,
    [DRAWER_CONTENT.COLOR]: <BackgroundColors />,
    [DRAWER_CONTENT.CUSTOM_FIELDS]: <CUSTOM_FIELDS />,
    //...그 외 다른 컴포넌트들...
     
//...
      
    <div>
      {contents[content]}
    </div>
  };

이 코드를 봤을 때는 특정 메뉴를 클릭했을 때 어떤 영역으로 이동해 어떤 컴포넌트를 보여주는지 쉽게 파악할 수 없습니다. 이를 알기 위해서는 각 컴포넌트로직을 따라가며 쿼리파라미터를 추가하는 코드를 확인해야 합니다.

예를 들어,위에서 언급된 플로우대로 change background 버튼을 클릭했을 때 표시되는 컴포넌트를 알기 위해서는 이렇게 접근해야 합니다.


  1. 전체 사이드 바 메뉴 중 change background를 클릭했을 때 어떤 쿼리파라미터를 추가하는지 파악
function DrawerMenuLists() {
  const [searchParams, setSearchParams] = useSearchParams();
  const handleContent = (content: string) => {
    searchParams.set(SEARCH_PARAMS_KEY.MENU, content);
    setSearchParams(searchParams);
  };
  return (
    <>
     //...
          <Menu.Button onClick={...}>About this board</Menu.Button>
          <Menu.Button onClick={() => handleContent(DRAWER_CONTENT.CHANGE_BACKGROUND)}>
     //...

  1. 해당 쿼리파라미터가 입력되었을 때 렌더링하는 컴포넌트 파악

  const contents = {
    [DRAWER_CONTENT.CHANGE_BACKGROUND]: <ChangeBackground />,
    

  1. ChangeBackground 컴포넌트에서 Photos를 클릭했을 때 표시하는 컴포넌트 파악
function ChangeBackground() {
  const [searchParams, setSearchParams] = useSearchParams();
  const handleContent = (content: string) => {
    searchParams.set(SEARCH_PARAMS_KEY.MENU, content);
    setSearchParams(searchParams);
  };
  return (
    <>
     //...
          <Menu.Button onClick={...}>About this board</Menu.Button>
          <Menu.Button onClick={() => handleContent(DRAWER_CONTENT.BACKGROUND_PHOTOS)}>
     //...
  1. 반복....

특히 유사한 컴포넌트명이 있거나 영역간의 이동이 겹치는 경우는 더욱더 혼란이 발생하게 됩니다.


응집도 측면에서의 단점: 상탯값이 흩어진 컴포넌트에 각각 관리되고 있음

이렇게 각각 컴포넌트가 흩어져있고, 그 컴포넌트 마다 영역간의 이동을 담당하는 로직을 각자 관리하고 있으면 구조 파악도 어렵고 유지보수 측면에서도 불리합니다. 즉 현재 방식은 로직의 응집도가 떨어집니다.
또한 각 뎁스별로 유저가 입력한 데이터 값을 관리해야 한다면 이 상탯값을 한 곳에 관리하는 것도 고려해야 합니다.
하나씩 뎁스를 거치며 유저가 선택한 데이터들을 모두 모아 관리해야 한다면, 이 값을 context API 또는 전역상태 관리 라이브러리를 사용해 별도로 모아 관리해야 합니다.



개선해봅시다

우선, 응집도 개선을 위해 상위 컴포넌트에서 모든 영역 이동 흐름을 관리할 수 있도록 한 곳에 로직을 작성합니다.
다른 컴포넌트에서도 공통적으로 사용할 것이기 때문에 커스텀 훅을 작성합니다.

커스텀 훅으로 추상화하기

해당 로직이 어떤 일을 하는지 추상화를 하면 다른 유사한 기능 구현에도 사용할 수 있을 뿐 아니라 기능을 이해하기도 쉬워집니다.
나아가 공통 모듈로 분리하거나 라이브러리로 만들면 사내 다른 프로젝트에도 공통적으로 적용할 수 있습니다.

이 커스텀 훅에서는 동일하게 쿼리파라미터를 사용합니다. 먼저 이를 핸들링하는 함수를 작성하겠습니다.

function useFunnel<T>(initialStep?: T) {
  const [searchParams, setSearchParams] = useSearchParams(`${SEARCH_PARAMS_KEY.FUNNEL_STEPS}=${initialStep}`);
  const step = searchParams.get(SEARCH_PARAMS_KEY.FUNNEL_STEPS);
  const navigate = useNavigate();

  const handleStep = (step: T) => {
    searchParams.set(SEARCH_PARAMS_KEY.FUNNEL_STEPS, step as string);
    setSearchParams(searchParams);
  };

그리고 한 상위 컴포넌트의 각 이동흐름을 알 수 있도록 하려면 먼저 하나의 상위 컴포넌트를 정의하고, 그 내부의 하위 컴포넌트들을 정의해 하나의 유저플로우에 해당하는 컴포넌트들을 한 눈에 파악할 수 있도록 합니다.

상위 컴포넌트에서는 현재 쿼리파라미터에 해당하는 하위 컴포넌트를 렌더링하도록 합니다.

  const Funnel = ({ children }: { children: ReactElement[] }) => {
    const targetStep = children.find((childStep: ReactElement) => childStep?.props?.name === step) ?? children[0];
    return <>{targetStep}</>;
  };

하위 컴포넌트에서는 인자로 전달받은 값으로 클릭했을 때 어떤 곳으로 이동할지를 결정합니다.

  const Step = ({ children, nextStep }: { children: ReactNode; name: T; nextStep?: T }) => {
    return <div onClick={() => nextStep && handleStep(nextStep)}>{children}</div>;
  };

  Funnel.Step = Step;

그 외 뒤로가기 등 필요한 로직들은 별도로 작성해주면 됩니다.
이 글에서는 생략하겠습니다!

그러면 만든 로직을 사용해보겠습니다.

  <Funnel>
  	<Funnel.Step name="change-background">
        <Menu.Title hasGoBackButton>Change Background</Menu.Title>
        <ButtonList>
            <Button onClick={() => handleStep("photos")}>Photos</Button>
            <Button onClick={() => handleStep("colors")}>Colors</Button>
        </ButtonList>
    </Funnel.Step>

    <Funnel.Step name="photos">
      <BackgroundPhotos/>
    </Funnel.Step>

    <Funnel.Step name="colors">
      <BackgroundColors/>
    </Funnel.Step>
  </Funnel>
  </div>
              

코드 개선 후 얻을 수 있는 장점

change background 메뉴 클릭 후 이와 관련된 영역 이동 흐름은 모두 이 곳에서 제어할 수 있습니다.

  • ChangeBackground에 대한 퍼널에는 changeBackground, photos, colors 세가지의 step이 있다는 것을 알 수 있습니다.즉, 이 유저 플로우와 관련된 컴포넌트를 한 눈에 파악할 수 있습니다.
  • 각각의 버튼을 클릭하면 어떤 step으로 이동하는지, 해당 step으로 이동하면 어떤 컴포넌트가 렌더링되는지 한 눈에 파악할 수 있습니다.


이번 글에서는...

토스에서는 이러한 패턴을 설문조사 패턴이라고 합니다.

이러한 방식을 더 유용하게 사용할 수 있는 경우는, 뎁스별 유저가 입력한 데이터를 관리해야할 때입니다. 대표적으로 설문조사나 회원가입, 로그인과 같은 데이터를 단계별로 한번에 하나씩 입력해 나가는 패턴을 예로 들 수 있습니다. 토스는 사용자가 간편하게 사용할 수 있도록 한 화면에 하나씩 데이터를 입력받는 것이 대부분이라 특히 많이 사용할 수 밖에 없는 것 같아요.

기능 구현에 앞서 좋은 코드 설계가 얼마나 중요한지 새삼 느낀 시간이기도 합니다.
디자인 패턴이나 이러한 설계방식에 대해서 틈틈히 공부하고 적용해 나가는 것 또한 좋은 프론트엔드 개발자로 성장할 수 있는 방법 중 하나인 것 같아요. 왜 컨퍼런스나 테크 블로그를 통한 다른 기업의 문제해결 사례를 꾸준히 보고 공부해나가야 하는지에 대해 느꼈던 계기이기도 했습니다.

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글