soyalattee 님의 상세페이지[2팀 박소연] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍

배포 페이지

과제의 핵심취지

  • React의 hook 이해하기
  • 함수형 프로그래밍에 대한 이해
  • 액션과 순수함수의 분리

과제에서 꼭 알아가길 바라는 점

  • 엔티티를 다루는 상태와 그렇지 않은 상태 - cart, isCartFull vs isShowPopup
  • 엔티티를 다루는 컴포넌트와 훅 - CartItemView, useCart(), useProduct()
  • 엔티티를 다루지 않는 컴포넌트와 훅 - Button, useRoute, useEvent 등
  • 엔티티를 다루는 함수와 그렇지 않은 함수 - calculateCartTotal(cart) vs capaitalize(str)

기본과제

  • Component에서 비즈니스 로직을 분리하기

  • 비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기

  • 뷰데이터와 엔티티데이터의 분리에 대한 이해

  • entities -> features -> UI 계층에 대한 이해

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

  • 특정 Entitiy만 다루는 함수는 분리되어 있나요?

  • 특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?

  • 데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?

심화과제

  • 재사용 가능한 Custom UI 컴포넌트를 만들어 보기

  • 재사용 가능한 Custom 라이브러리 Hook을 만들어 보기

  • 재사용 가능한 Custom 유틸 함수를 만들어 보기

  • 그래서 엔티티와는 어떤 다른 계층적 특징을 가지는지 이해하기

  • UI 컴포넌트 계층과 엔티티 컴포넌트의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

  • 엔티티 Hook과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

  • 엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

과제 셀프회고

리팩토링 과제를 하며, 평소 개발하면서 느끼는 고민이 떠올랐습니다. 코드를 작성할 때 항상 함수는 하나의 역할만 해야 한다는 단일 책임 원칙을 지키려 노력하지만, 요구사항이 점점 추가되고 예외 케이스가 생기면서 어느새 함수가 여러 역할을 떠안은 비대한 형태가 되곤 합니다.

그 과정에서 상태값이 여러 곳에서 변경되고, 상태 추적이 어려워지는 경험도 자주 했습니다. 그래서 이번 과제에서 처음 마주한 코드가 낯설지 않았습니다. 제 코드들도 자주.. 이렇게 책임을 가득 안은 복잡한 컴포넌트들이 되어버리곤 합니다.

이렇게 복잡해진 컴포넌트는 실제 서비스에 반영된 뒤엔 손대기가 더 어려워집니다. "괜히 수정했다가 버그 생기면 어쩌지?" 하는 두려움에 리팩토링도 점점 소심해집니다..

그런 의미에서 테스트하기 좋은 코드가 좋은 코드라는 말이 이번 과제를 통해 더 와닿았습니다. 테스트 코드를 통해 로직의 정확성을 검증할 수 있을 뿐 아니라, 응집도는 높고 결합도는 낮아 이해하기 쉽고, 결합도 낮은 구조로 만들 수 있기 때문입니다. 결국, 이런 구조가 나 자신에게도 자신 있는 코드가 되는 것 같습니다.

🛠 내가 세운 리팩토링 목표

그래서 이번 과제에서는 요구사항이 추가되어도 마음 편히 수정할 수 있는 코드를 만드는 것을 목표로 삼았습니다. 몇 달 뒤 내가 다시 봐도 플로우를 따라가기 쉬운 코드, 다른 개발자가 봐도 "이렇게 수정하면 되겠구나" 하는 코드가 되도록 노력했습니다.

이를 위해 아래 세 가지 원칙을 중점적으로 지켰습니다:

  • Component와 비즈니스 로직의 분리

  • 가능한 로직은 순수 함수로 만들기

  • 단일 책임 원칙 지키기

또한 지난 과제에서는, 함수나 상태의 역할을 충분히 이해하지 못한 채 무작정 분리했다가 후반에 고생한 기억이 있습니다. 이번에는 같은 실수를 반복하지 않기 위해 각 함수와 상태가 어떤 역할을 하는지 정확히 이해한 뒤 리팩토링을 시작했습니다.

과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?

이번 과제에서 제일 신경 쓴 건 코드 구조를 잘 나누는 것, 그중에서도 응집도는 높이고 결합도는 낮추는 방향으로 설계하는 것이었습니다.

지난 과제에서도 비슷한 고민을 했었는데, 그때는 코드를 먼저 분리하고, 책임에 대한 이해 없이 구조를 바꾸다 보니 되려 더 복잡해졌던 기억이 있어서, 이번엔 왜 나누는지, 각 파트가 어떤 역할을 해야 하는지를 먼저 고민하고 시작했습니다.

구조 분리에 제일 많은 고민을 했습니다

우선 전반적인 폴더 구조를 datas(엔티티), features/hooks(storeHooks), components(UI) 세 층으로 나누고 각자 책임을 분리했습니다.

예를 들어 상품, 장바구니, 쿠폰 관련 로직은 각각 독립된 훅으로 나누고, UI 쪽은 props 기반으로 처리해서 store 훅에 직접 의존하지 않도록 했어요. 이렇게 하면 나중에 UI만 따로 패키지로 분리하거나 재사용할 때도 훨씬 유리하겠다고 생각했습니다.

정리하자면, 이번 과제에선 “나중에 바꾸기 쉬운 구조”를 목표로, 폴더 구조 정리 → 상태관리 기준 세우기 → 결합도 낮추기 위한 추상화 이 세 가지를 중심으로 제일 많이 고민하고 신경 썼습니다.

App → Layout → Header 구조를 깔끔하게 분리했습니다.

처음에는 App 컴포넌트 안에 모든 UI와 로직이 다 섞여 있어서, 흐름을 한눈에 파악하기 어려웠습니다. 그래서 공통 레이아웃 역할만 하는 Layout 컴포넌트를 따로 만들고, Header, NotificationContainer, 그리고 페이지 내용(children)을 prop으로 넘겨서 구성했습니다.

// App.tsx 일부 발췌
return (
  <Layout
    Header={
      <Header
        isAdmin={isAdmin}
        searchTerm={searchTerm}
        setSearchTerm={setSearchTerm}
        toggleAdmin={toggleAdmin}
      />
    }
    NotificationContainer={
      <NotificationContainer
        notifications={notifications}
        removeNotification={removeNotification}
      />
    }
  >
    {isAdmin ? (
      <AdminPage addNotification={addNotification} />
    ) : (
      <CustomerPage
        debouncedSearchTerm={debouncedSearchTerm}
        addNotification={addNotification}
      />
    )}
  </Layout>
);

이렇게 구성하면 App은 페이지 흐름만 담당하고, 실제 UI는 외부에서 주입되는 구조라서 어떤 페이지든 공통 레이아웃만 씌우면 재사용이 가능하고, 구조도 파악하기 쉽도록 했습니다.

그리고 Layout은 정말 레이아웃만 담당하는 순수한 컴포넌트로 만들었습니다.

// Layout.tsx
const Layout = ({ Header, NotificationContainer, children }: LayoutProps) => {
  return (
    <div className="min-h-screen bg-gray-50">
      {Header}
      {NotificationContainer}
      <main className="max-w-7xl mx-auto px-4 py-8">{children}</main>
    </div>
  );
};

내부 상태나 훅은 전혀 없고 배치만 깔끔하게 처리하여 역할을 명확하게 하고, 재사용성을 높혔습니다.

이런 식으로 구조를 계층적으로 나누고 역할을 분리하니 코드가 확실히 읽기 쉬워지고 유지보수도 부담이 줄었다고 생각합니다.

과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?

  • useSyncExternalStore도 처음 써봤는데, 구조 자체는 흥미로웠지만 이걸 꼭 써야 했던 상황이었는지 다시 생각해보면 좋겠다는 생각도 들었어요. 심화과제에서 jotai를 도입하면서 굳이 중복 구조로 관리할 필요가 있었을까? 하는 고민이 있었습니다.

  • addNotification 같은 외부 콜백을 훅에 직접 넘겨주는 방식이 좀 섞여 있었는데요, 이건 나중에 테스트할 때나 알림 시스템이 바뀔 때 유지보수가 어려워질 수 있다는 피드백을 받았습니다.


useAdminNotifications(addNotification)

직접 넘기지않고 onSuccess/onError 콜백을 받는 식으로 인터페이스를 통일했다면 더 깔끔했을 것 같습니다.

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문

  • 상품, 장바구니, 쿠폰 등의 데이터를 localStorage에 저장하고 관리하기 위해 useSyncExternalStore를 사용해보았습니다. 이번 항해를 통해 처음 알게 된 훅이라 직접 적용해보았는데, 이러한 경우에 useSyncExternalStore를 사용하는 것이 적절한 선택이었는지 궁금합니다. 혹시 더 적합한 방식이 있다면 조언 부탁드립니다.

기존 localStorage Hook 1bc2db2c9c6d6fe4782f8c17152cd8360479c7dd 변경된 useSyncExternalStore Hook 1bc2db2c9c6d6fe4782f8c17152cd8360479c7dd

  • 현재 상태관리 라이브러리(Jotai)는 알림(Notification) 상태만 관리하는 데 사용하였고, 나머지 상태는 각 컴포넌트 내부 혹은 별도의 훅을 통해 관리했습니다. 또 다른 상태들도 atom으로 관리했어야 했을지 고민이 되었습니다. 다만, 상태관리 라이브러리가 단순히 props drilling을 해결하기 위한 수단만은 아니라고 생각하고 있어서 어디까지 상태관리 라이브러리에서 관리하면 괜찮을지 의견 궁금합니다..!

과제 피드백

수고하셨습니다. 소연님!

Q. 상품, 장바구니, 쿠폰 등의 데이터를 localStorage에 저장하고 관리하기 위해 useSyncExternalStore를 사용해보았습니다.
이번 항해를 통해 처음 알게 된 훅이라 직접 적용해보았는데, 이러한 경우에 useSyncExternalStore를 사용하는 것이 적절한 선택이었는지 궁금합니다. 혹시 더 적합한 방식이 있다면 조언 부탁드립니다.

A. useSyncExternalStore를 활용해서 로컬스토리지를 연동한 것은 좋아 보입니다. 이렇게 하면 렌더링시 문제점도 많이 개선될 수 있을 것 같아요. 현재 상황에서는 적절한 방법이었다고 생각합니다. 물론 이 훅을 이용하지 않아도 정상 동작은 하게 만들 수 있겠지만 지금 코드는 좀 더 나아보입니다 :) 멋집니다. 소연님 :)

Q.현재 상태관리 라이브러리(Jotai)는 알림(Notification) 상태만 관리하는 데 사용하였고,
나머지 상태는 각 컴포넌트 내부 혹은 별도의 훅을 통해 관리했습니다. 또 다른 상태들도 atom으로 관리했어야 했을지 고민이 되었습니다.
다만, 상태관리 라이브러리가 단순히 props drilling을 해결하기 위한 수단만은 아니라고 생각하고 있어서 어디까지 상태관리 라이브러리에서 관리하면 괜찮을지 의견 궁금합니다..!

A. 넵 맞아요 단순히 상태관리도구는 프롭드릴링을 해결하려고 사용한다기보다는 상태들을 다루는 로직이나 비즈니스 로직을 분리해 재사용할 수 있게 만드는 역할도 큽니다. 사실 사용하기 나름이에요. 그래서 상태관리도구를 억지로 쓴다기보다는 필요할때 쓴다가 적절할 것 같아요. 일부 데이터를 다루는 로직의 재사용을 위해서, 혹은 프롭드릴링을 위해서 등 필요할 때 고려하셔도 될 것 같아요. 요즘은 React Query같은 도구로 인해 프랍드릴링도 캐싱기반의 패칭과 재가공으로 공유하기 때문에 역할이 조금 더 줄었어요.