Skip to content

[1팀 신희원] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 #36

Open
Amelia-Shin wants to merge 33 commits intohanghae-plus:mainfrom
Amelia-Shin:main
Open

[1팀 신희원] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 #36
Amelia-Shin wants to merge 33 commits intohanghae-plus:mainfrom
Amelia-Shin:main

Conversation

@Amelia-Shin
Copy link

@Amelia-Shin Amelia-Shin commented Aug 4, 2025

배포링크 : https://amelia-shin.github.io/front_6th_chapter2-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과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

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

과제 셀프회고

4주차 때 바이브코딩을 하다보니 엄청 현타가 왔었다. 내 생각대로 되지 않는 AI... 코드를 짜줘도 맘에 들지 않아...과제 제출하고 테스트 통과하기 위해 맘에 안들지만 계속 진행시켜... 이런식으로 과제를 진행하다 보니 스스로 이번 교육의 목적을 잊은채로 나 뭐하고 있는거지?? 했다. 테스트 통과가 목적이 아니라 스스로 배워가는게 크다는 것을 왜 잊었을까!! (근데 또 막상 테스트 실패하면 과제 fail나서 이것도 이거 나름대로 현타가 옴 ㅜㅜ -> 1주차때 과제를 다 못끝내 엄청 아쉬움이 많았지만 배워가는데에 의의를 두자 ! 라고 맘 편히 먹자 했지만 과제 결과로 2fail 뜬 것을 보니 아쉬움 x 99999 와 마음이 편안하지 못했다.)

4주차 때 엄청난 현타로 인해 5주차 때는 다시 초심으로 돌아가서 내가 배워가는데 의의를 두고, AI한테 짜달라하지 말고 간단한 작업만 도움을 받자! 라는 마음으로 진행했다. 처음에 과제를 진행하면서 막히는 부분도 꽤 많았지만 같은 팀원분들과 같이 코드 리뷰도 하고 서로의 코드를 공유하며 스스로 생각했을 때 상대방 코드가 더 좋은거 같다 생각이 들면 직접 내 코드에 적용해보는 등 하나하나 부딪혀가고 배워갔다. 내가 이렇게 물어보는게 어떻게 보면 상대방의 시간을 뻇는 것이라 생각이 들었지만, 오히려 나한테 개념이랑 코드를 설명해주면서 그 사람도 알고 있던 지식응 공유해주면서 실수를 바로 잡거나 새로 배워가지 않았을까? 서로 win-win관계이지 않았을까? 라고 생각하며 그냥 열심히 물어보면서 이번 과제를 진행했다. 4주차보단 이번 주차 과제에서는 흥미와 재미를 좀 느끼며 진행했다.

다음 주차도 페어코딩(?) 을 하면서 더 많은 것을 배우고 싶다!

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

  • 컴포넌트 분리 ( UI 와 Hooks ) -> 더 세세하게 나누는게 좋았을까(ProductImg , ProductDiscount, ProductPrice 등등) ? 큼직큼직하게 나누는게 (ProductCard < ProductList ) 좋았을까?

  • 도메인별로 컴포넌트를 나눌때, 낮은 결합도, 높은 응집도 를 지키기
    -> Product, Cart, Coupon 에 대해서 응집도를 높이기 위해서 어떻게 하면 더 잘 분리할 수 있을까? 고민이 들었던거 같다.

  • 배워가는 과정 (Custom Hook, 전역상태 (localStorage 와 상태 연동) 등등)

🔍 Custom Hooks 상태 공유 실수
Props drilling을 피하기 위해 하위 컴포넌트에서 같은 커스텀 훅을 다시 선언해서 사용했었다.

// 예시 코드 
function ParentComponent() {
  const { addNotification } = useNotification();
  
  addNotification('성공');
  return (
    <div>
      <ChildComponent />  {/* props로 전달하지 않음 */}
    </div>
  );
}

function ChildComponent() {
  const { addNotification } = useNotification(); // 같은 훅을 다시 선언
  
}

발생한 문제점 : 각 컴포넌트마다 독립적인 상태가 생성
Parent에서 addNotification 값이 변경되었음에도 불구하고, Child에서 addNotification은 값이 변하지 않았다. (같은 훅을 사용하는데도 완전히 분리된 두 개의 상태가 존재)

이 부분을 2번이나 실수했고... 1팀에 휘린님에게 문제 해결을 도와달라고 부탁하여 같이 진행해보았다.
그러면서 배우게 된 점!!! (2번이나 실수하니 확실히 배우게 되었다 :) ) -> 리액트 훅의 기본 원칙!

커스텀 훅을 호출할 때마다 새로운 상태가 생성된다!

useState는 호출될 때마다 새로운 독립적인 상태를 만듦
커스텀 훅은 단순히 로직을 재사용하는 것이지, 상태를 공유하는 것이 아님 ( = 커스텀 훅은 로직 재사용을 위한 도구)
각 컴포넌트는 자신만의 상태 인스턴스를 가지게 됨

🔍 localStorage와 React State 동기화 딜레마
핵심 문제: 두 개의 독립적인 상태 저장소

React state: 컴포넌트 마운트/언마운트 시 생성/소멸
localStorage: 브라우저 세션을 넘어 영구 지속

이 둘을 동기화하는 과정에서 다음과 같은 질문들이 떠올랐다.
"상태 변경 시점에 즉시 localStorage에 저장해야 하는가?"
"초기 로드 시 localStorage 값이 React state를 덮어써야 하는가?"

처음에 내가 접근한 방식

const [value, setValue] = useState(() => {
  const saved = localStorage.getItem(key);
  return saved ? JSON.parse(saved) : initialValue;
});

React state  localStorage 동기화  
useEffect(() => {
  localStorage.setItem(key, JSON.stringify(value));
}, [value, key]);

이 방법을 썼더니, 상태 업데이트 시 일관성이 보장되지 않았다.

그래서 같이 페어코딩을 하던 1팀(휘린, 아름)과 코드 공유를 하면서 어떻게 해결하면 되는지 알게 되었다. (Single Source of Truth 사용)

const setValue = (value: T | ((val: T) => T)) => {
    setStoredValue((prev) => {
      // value는 T 타입이거나, (prev: T) => T 형태
      // 함수면 이전 상태 prev를 넣어 새 상태를 계산
      const newValue =
        typeof value === "function" ? (value as (val: T) => T)(prev) : value;

      if (
        // 빈 배열이나 undefined면 localStorage에서 제거
        newValue === undefined ||
        (Array.isArray(newValue) && newValue.length === 0)
      ) {
        localStorage.removeItem(key);
      } else {
        // 그 외엔 JSON으로 직렬화해서 localStorage에 저장
        localStorage.setItem(key, JSON.stringify(newValue));
      }

      return newValue;
    });
  };

내가 생각한 위 두 코드의 차이점을 작성해본다.
첫번째 코드는 useEffect로 상태값(key, value)이 변할 때 localStorage에 값을 바꿔주기만 하고 (변한 상태값 반환 X),
두번째 코드는 상태값이 변했을 때 localStorage를 업뎃해준뒤 변한 상태값을 반환해준다.

좀 더 찾아보니 실행 시점에도 차이가 있었다.
첫번째 : React state 업데이트 → 렌더링 → useEffect 실행
두번째 : localStorage 저장 → React state 업데이트 요청 (동기)

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

원래의 목표는 과제를 진행하면서 몰랐던 부분들을 열심히 배워가면서 과제를 하는 것이었다. 하지만 배워가면서 진행하다 보니 진도가 너무 뒤쳐지고 있었다. 결국엔 과제 제출일에 가까워 질 수록 테스트 통과가 목표가 되어 결국엔 다른 사람의 PR을 보거나 AI를 활용하여 스스로 생각하는 기회가 적어졌다.
내가 더 과제에 시간을 투자하고 진행했다면, 더 많이 배워가고 스스로 생각하는 힘이 길러졌을 텐데... 내 노력과 배움이 잘 녹아든 과제가 될 수 있었을텐데.. 이런 부분에서 좀 아쉬워서 꼭 다음에는 스스로 생각하면서 과제를 오로지 내 힘으로만 진행해보고 싶다. ( 꼭!!! )
-> 왜 클린코드 주차 과제에서는 아쉬움만 크게 남는걸까...ㅠㅠ

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

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

Q1. 제가 위에 배워가는 과정에 작성한 내용이 맞는 내용인지 궁금합니다! (혹시나 틀린 개념이 있거나 더 추가보완해서 설명해주실 것이 있다면 말씀 부탁드립니다.)
Q2. 과제 관련한 질문은 아니지만, 현재 항해를 5주차 진행했는데요. 남은 기간동안 취업을 위해 무엇을 하면 좋고, 남은 항해과정을 어떻게 나아가면 좋을지... (교육을 더 잘 흡수하기 위한 꿀팁같은게 있을지..) 궁금합니다!

- 항상 최신 상태 기반 업데이트 가능
- 헤더를 별도 컴포넌트로 분리
- useSearchTerm 훅으로 검색 로직 분리
- CartIcon 컴포넌트 생성
- 중복 알림 방지를 위한 메시지 필터링 로직 추가
- useRef를 활용한 안전한 타이머 관리 시스템 구현
- 메모리 누수 방지를 위한 타이머 정리 로직 추가
- 타입 오류 수정 (Notification → NotificationType)
- 수동 제거 시 타이머 정리 기능 개선
- useState를 jotai atom으로 교체
- props drilling 제거하고 직접 atom 접근
- App.tsx 간소화 (props 전달 제거)
- isAdminAtom, totalItemCountAtom 추가
- 모든 컴포넌트에서 직접 hooks 사용
Copy link

@JunilHwang JunilHwang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요, 과제 잘 진행하셨습니다 — 구조화(components / hooks / ui / icons / store 등)와 테스트 보강(Jotai Provider 추가) 모두 의미 있는 리팩토링이에요. 아래는 요청하신 흐름(변화 시나리오 → 각 시나리오 적용 시 코드 영향 → 응집도/결합도 관점에서 평가 및 개선안)을 따라서 정리한 PR 리뷰입니다. 그대로 GitHub PR 코멘트로 붙여넣기 하셔도 됩니다.

요약: 키워드 → PullRequestBody 회고 피드백(인사이트) → "리뷰 받고 싶은 내용" Q1/Q2 답변 → 상세 피드백(정의·문제·AS-IS·TO-BE) → 우선순위 개선 체크리스트


안녕하세요! PR 잘 만드셨어요. 전체적으로 도메인 단위(Products / Coupons / Cart / UI / Notification)로 폴더와 훅을 분리한 점, 테스트에서 Provider를 넣어 격리한 점이 특히 마음에 듭니다.
아래는 PR에 남길 종합 피드백과 상세 피드백입니다.

질문에 대한 답변 (PullRequestBody)

  • Q1. 배워가는 과정에 쓴 내용(커스텀 훅은 호출마다 상태가 생긴다 / useState 호출 시마다 새로운 상태 인스턴스 / localStorage 싱크 전략)은 맞습니다. 추가 보완:
    • “커스텀 훅은 로직 재사용” → 맞음. 상태를 전역적으로 공유하려면 전역 store/컨텍스트/atom 등을 사용해야 합니다. (커스텀 훅 내부에서 전역 store를 사용하면 그 훅을 여러 컴포넌트에서 호출해도 같은 전역 상태를 참조합니다.)
    • localStorage 싱크: 현재 PR에서 잘 보완하신 것처럼, "상태를 변경하는 API에서 localStorage를 같이 업데이트" 하는 방식(Single Source of Truth)이 안전합니다. 단, 이때도 동시성(여러 탭)이나 복원 로직(초기화 시 로컬스토리지 -> 상태 반영) 정책을 명확히 해야 합니다.
  • Q2. 항해 남은 기간(취업 준비/학습 팁)
    • 계속 실무형 연습: 작은 서비스(제품 목록 + 장바구니 + 주문)를 한 번 완성(에2~3주)하고, 기술 스택(상태관리 2개, 테스트, 빌드 배포)을 바꿔가며 리팩토링해보세요. PR/리뷰/README 작성 연습도 추천합니다.
    • 인터뷰 준비: 구현한 코드(특히 훅/아키텍처 결정을 문서화)와 trade-off를 설명할 수 있으면 좋습니다(왜 Jotai를 썼는가, 전역상태를 왜 분리했는가 등).
    • 실전 팁: 작은 기능 단위로 TDD → 리뷰 → 리팩토링 사이클을 반복하면 ‘설계 관점’이 빨리 성장합니다.

종합 피드백 (PullRequestFiles에서 뽑은 키워드)

  • 키워드(문제/개선 포인트): 상태추상화(Jotai atoms → hooks), Notification API 디자인, localStorage 동기화 패턴, 컴포넌트/훅의 계층 분리, 모듈화/패키지화 준비, 테스트 격리(jotai Provider), prop drilling 회피 시 실수(훅 재선언), 재사용성(핵심 훅의 인터페이스), 네이밍(setNotifications vs removeNotification), UI와 도메인 책임 분리.
  • 전반 평: 도메인 단위로 폴더 잘 나눈 점은 응집도가 높아지는 방향입니다. 다만 내부 API(훅 반환값) 설계에서 약간의 결합과 혼동 지점(예: setNotifications의 역할 모호성, addNotification vs setNotifications 등)이 보입니다. 이 부분만 정리하면 패키지화 / 상태관리 라이브러리 교체가 훨씬 쉬워집니다.

종합 피드백 (PullRequestBody의 회고/신경 쓴 부분에 대한 인사이트)

  • 회고 인사이트:
    • 본인이 겪은 ‘테스트 통과가 목적이 되던 상황’ → 좋은 관찰입니다. 단기 목표(테스트 통과) vs 장기 목표(개념 습득) 충돌은 교육 과정에서 자주 발생합니다. 이를 피하려면 "학습 목표별 체크리스트(예: 이번 PR은 훅 인터페이스 설계 연습)"를 미리 정해두는 게 좋습니다.
    • Props drilling을 피하려 훅을 여러 컴포넌트에서 호출해 생긴 문제를 직접 겪고 해결한 경험은 매우 큰 수확입니다. 이 경험을 문서(README)에 정리하면 나중에 같은 실수 재발 방지에 도움이 됩니다.
  • 더 생각해볼 질문:
    1. 현재 전역 상태(예: cart, notifications)의 '소유자'(single source)는 어디인가? 그 소유자를 명확히 할 수 있는가?
    2. 특정 로직(예: localStorage 동기화, notification 제거 정책)은 훅 내부에서 처리할까, 아니면 별도의 adapter로 분리할까?
    3. 각 훅의 public API가 바뀌면(예: addNotification 이름 변경) 영향 범위는 얼마나 되는가? (응집도/결합도 체크 포인트)
  • PR에 적은 “컴포넌트 분리” 고민: 훅/컴포넌트 모두 '의미 단위'로 나눈 건 괜찮습니다. 더 세세하게 쪼갤지, 큼직하게 유지할지는 재사용성과 유지보수성을 보고 결정하세요. (예: ProductImg/Price/Discount는 재사용/테스트 필요성이 있으면 쪼개세요.)

"리뷰 받고 싶은 내용"에 대한 답변

  • Q1 (개념 맞는지): 네, 개념(커스텀 훅과 상태 인스턴스, localStorage 싱크 전략 등)은 전반적으로 맞습니다. 추가: 훅이 상태를 '공유'하도록 만들려면 단순 훅 재사용이 아니라 전역 스토어나 Context/atom을 넣어야 합니다. 코드 레벨에서의 투명한 API(예: addNotification vs onSuccess/onError)를 권장합니다.
  • Q2 (취업/학습 조언): 위 회고에 쓴 내용 참고. 실무 대비로는 "설계 결정을 문서화"하고 "한 번에 하나의 기술을 바꿔 리팩토링" 연습(예: Jotai→Zustand→Redux로 순차 교체)하면서 각 바꾸는 시점의 변경 목록, 테스트 영향, 빌드 크기 등을 기록해보세요.

상세 피드백 (개념 정의 → 문제 → AS-IS → TO-BE)

먼저 개념 정의 (리뷰에서 사용할 기준)

  • 응집도(cohesion): 단위(파일/모듈/패키지)가 하나의 책임(또는 매우 관련된 책임)만 가지는 정도. 실용 정의(요청하신 규칙 기준):
    1. 변경에 대한 동선(파일 수정/추가/삭제 경로)이 짧은가? (짧을수록 응집도 높음)
    2. 라이브러리로 만들 때 해당 모듈을 매끄럽게 떼어낼 수 있는가?
  • 결합도(coupling): 모듈/컴포넌트/함수들이 서로 직접적으로 얼마나 의존하는가. 낮을수록 좋음. 좋은 방법: "인터페이스(옵션 콜백, 단순 액션 함수, 타입)"를 통해 결합도를 낮춤.

이제 파일 기반 관찰을 근거로 각 이슈를 정리합니다.

  1. Notification API / 훅 반환값 네이밍 및 역할
  • 개념: 결합도 문제 — 훅이 내부 상태의 'set'을 그대로 노출하면 소비자가 내부 구현(형태)에 의존한다. 더 좋은 방법은 '행동(action)'을 표현하는 API를 노출하는 것.

  • 문제 정의:

    • 현재: useNotification 훅에서 { notifications, setNotifications } 형태로 사용되는 곳과, addNotification 형태로 사용되는 곳이 혼재합니다. Notification component expects setNotifications(id) signature (removes by id), 훅은 setNotifications으로 일반 setter를 내려줄 가능성이 있음(이름 혼선).
    • 결과: 다른 개발자가 setNotifications을 'setter'로 이해할 수 있고, Notification 컴포넌트가 내부 로직(특정 동작)을 직접 수행하게 만듦 → 결합 증가.
  • AS-IS (요약)

    • useNotification 제공: { notifications, setNotifications } 또는 { notifications, addNotification } 혼합
    • Notification component 사용: setNotifications(notif.id) (즉 "remove" 기능으로 호출)
  • 문제 예시 코드 (AS-IS)

    // App.tsx
    const { notifications, setNotifications } = useNotification();
    <Notification notifications={notifications} setNotifications={setNotifications} />
    // Notification.tsx
    const Notification = ({ notifications, setNotifications }) => (
      notifications.map(n => <button onClick={() => setNotifications(n.id)} />)
    )
  • TO-BE(권장)

    • 훅은 명확한 행동(action) API를 내보내세요: addNotification, removeNotification, clearNotifications 등.
    • Notification 컴포넌트에는 removeNotification(id) 또는 onDismiss(id) 같은 semantic한 prop을 주기.
  • TO-BE 코드

    // useNotification.ts
    export const useNotification = () => {
      const [notifications, setNotifications] = useState<Notif[]>([]);
      const addNotification = (msg, type = 'success') => { ... };
      const removeNotification = (id) => setNotifications(prev => prev.filter(x => x.id !== id));
      return { notifications, addNotification, removeNotification, clearNotifications: () => setNotifications([]) };
    };
    
    // App.tsx
    const { notifications, addNotification, removeNotification } = useNotification();
    <Notification notifications={notifications} onDismiss={removeNotification} />
  • 이점: API가 의도(행동)를 드러내므로 호출부가 내부 상태 형태에 의존하지 않음(낮은 결합도).

  1. localStorage 동기화 / Single Source of Truth 패턴
  • 개념: 응집도와 책임 분리 — 저장(adapter) 책임을 훅/스토어 내부로 몰아야 소비자가 구현 세부를 몰라도 됨.
  • 문제 정의:
    • PR 초반 고민에서 보였듯 useEffect로만 sync하면 렌더 타이밍 때문에 불일치 발생. PR은 훅/스토어 내부에서 setter가 localStorage를 동기화하도록 리팩토링한 것처럼 보입니다(좋음).
    • 다만 여러 훅들이 localStorage 접근을 흩어두면 유지보수성이 떨어짐(응집도 저하).
  • AS-IS
    • 다수의 useEffect 혹은 useState initializer에서 localStorage 직접 접근(이전 코드에 다수 존재).
  • TO-BE (권장)
    • storage adapter(유틸) 하나로 추상화하고, 훅/스토어에서 그 adapter를 사용.
    • 또는 전역 store의 set 함수에서 localStorage를 한 번만 업데이트.
  • 예시 TO-BE 코드
    // storage.ts
    export const storage = {
      get: (key) => { try {...} },
      set: (key, value) => { if(empty) localStorage.removeItem(key) else localStorage.setItem(key, JSON.stringify(value)) },
    };
    
    // usePersistentState.ts
    export const usePersistentState = (key, initial) => {
      const [state, setState] = useState(() => storage.get(key) ?? initial);
      const setPersistent = (updater) => {
        setState(prev => {
           const next = typeof updater === 'function' ? updater(prev) : updater;
           storage.set(key, next);
           return next;
        });
      };
      return [state, setPersistent];
    };
  • 이점: 모든 로컬저장 정책을 한곳에서 바꾸면 됨(응집도 증가, 결합도 감소). 탭간 sync가 필요하면 storage adapter에 broadcast/StorageEvent 처리를 추가하면 됩니다.
  1. 훅 인터페이스(전달 인자) — 결합도 관점
  • 문제 정의:

    • 일부 훅/컴포넌트는 addNotification 같은 구체적 네이밍을 prop으로 직접 전달합니다. 사용성은 간단하지만, 함수 이름에 의존하게 되어 바꿀 때 영향 범위가 넓습니다.
    • 예시의 안 좋은 예시(README에 있는 것처럼):
      const useAddProduct = (addNotification) => { ... }
      // 이렇게 되면 addNotification이라는 용어/동작에 강하게 결합
  • 권장(TO-BE)

    • 콜백 기반의 옵션 인터페이스로 추상화: onSuccess, onError, onComplete 등.
    • 또는 EventEmitter/Callback object.
  • TO-BE 코드

    // 좋은 예
    const useAddProduct = ({ onSuccess, onError } = {}) => {
      const execute = async (product) => {
        try {
          // ...추가
          onSuccess?.(product);
        } catch (e) {
          onError?.(e);
        }
      };
      return { execute };
    };
    
    // 사용부
    const { execute: addProduct } = useAddProduct({
      onSuccess: () => addNotification('상품 추가됨'),
    });
  • 이점: 훅 내부에서 알림 전략을 바꿔도 호출부는 onSuccess/onError만 유지하면 됨.

  1. State 라이브러리 교체 시 시나리오 및 영향 (요청하신 시나리오)
    아래는 "현재 코드가 Jotai 기반이다"라는 전제 하에 다른 상태관리로 바꿀 때 필요한 변경·마이그레이션 포인트와 예시 코드(AS-IS/TO-BE)를 정리했습니다.

상황 A: jotai → zustand

  • 영향 요약:
    • 현재 useAtom / atoms 참조를 사용하는 훅(예: useCart)은 zustand store로 옮겨야 합니다.
    • 컴포넌트는 대체로 useCart() 같은 훅을 통해 상태를 소비하므로, 훅 내부 구현을 바꿔도 호출부는 그대로 유지하면 이상적입니다(즉 훅 추상화가 잘 되어 있으면 작업량 적음).
  • 변경 포인트:
    1. atoms 파일 삭제/변환 -> create()로 스토어 생성
    2. useCart 훅을 store wrapper로 변환(혹은 store 자체를 export)
    3. 테스트: Provider 제거/대체(직접 스토어 초기화)
  • AS-IS (Jotai)
    // store/atoms.ts
    export const cartAtom = atom<CartItem[]>([]);
    // useCart.ts
    export const useCart = () => {
      const [cart, setCart] = useAtom(cartAtom);
      return {
        cart,
        addToCart: (p) => setCart(prev=>...),
      }
    }
  • TO-BE (Zustand)
    // store/cartStore.ts
    import create from 'zustand';
    export const useCartStore = create(set => ({
      cart: [],
      addToCart: (product) => set(state => ({ cart: [...state.cart, { product, quantity: 1 }] })),
      // ...other actions
    }));
    
    // useCart.ts (wrapper, optional)
    export const useCart = () => {
      const { cart, addToCart, removeFromCart } = useCartStore();
      return { cart, addToCart, removeFromCart };
    };
  • 추가 권장: store 안에서 localStorage 동기화를 담당하도록 만들면 각 컴포넌트가 신경 쓸 필요 없음.

상황 B: jotai → tanstack-query (React Query)

  • 영향 요약:
    • Tanstack Query는 주로 서버 state/비동기 캐싱용입니다. 로컬 클라이언트 상태(장바구니)는 React Query로도 관리할 수 있지만, 그 경우 queryClient.setQueryData / useMutation API를 사용.
    • 비용: 로컬 상태로 사용하면 코드 패턴이 바뀌고 persistence/optimistic update 처리가 필요.
  • 변경 포인트:
    1. 서버 통신/캐시용 로직은 useQuery로 옮기고 캐시로 read/write.
    2. addToCart, updateQuantity는 useMutation로 처리.
  • 예시 TO-BE
    const CART_KEY = ['cart'];
    // 조회
    const { data: cart = [], refetch } = useQuery(CART_KEY, () => fetchCartFromLocalOrServer());
    // 변경
    const mutation = useMutation(newItem => saveCart([...cart, newItem]), {
      onSuccess: () => queryClient.invalidateQueries(CART_KEY)
    });
  • 권장: 장바구니 같은 빠른 로컬 데이터는 React Query보다 전역 store(또는 Jotai/Zustand)로 유지하고, 상품목록/서버데이터는 React Query로 두는 혼합 전략이 일반적입니다.

상황 C: jotai → redux/toolkit

  • 영향 요약:
    • atoms → slice로 마이그레이션. Provider 교체. useSelector/useDispatch로 호출부 변경.
  • 변경 포인트:
    1. slice 생성(초기State, reducers)
    2. 훅(useCart)을 useSelector/useDispatch 래퍼로 변경
  • 예시 TO-BE
    // cartSlice.ts
    const cartSlice = createSlice({ name: 'cart', initialState: [], reducers: { add: (state, action) => {...} }});
    // useCart.ts
    export const useCart = () => {
      const cart = useSelector(state=>state.cart);
      const dispatch = useDispatch();
      return { cart, addToCart: (p) => dispatch(cartSlice.actions.add(p)) }
    }

결론(상태 라이브러리 변경 대비 조언)

  • 훅이 "추상화 계층" 역할을 잘 하도록(components -> useCart -> store 구현) 설계되어 있으면, 내부 store를 바꾸더라도 컴포넌트는 건드릴 필요가 거의 없습니다. 지금 PR은 이 방향으로 구성된 부분이 많아 마이그레이션 난이도가 낮아 보입니다(좋음).
  • 반대로 컴포넌트가 직접 atoms를 import/사용하면 마이그레이션 비용이 큽니다. (현재는 대부분 훅 래퍼를 사용하므로 잘 되어 있음)
  1. 모듈화/패키지화 준비(응집도/결합도 점검)
  • 어떤 코드를 묶어야 하는가?
    • entities (types + pure entity functions like calculateCartTotal 등)
    • hooks (useCart, useProducts, useCoupons, useNotification, useSearchTerm)
    • ui components (ProductCard, CartList, CouponSelector, Notification, Header)
    • utils (storage adapter, validators, formatters)
  • 응집도 평가(요청하신 기준)
    • 파일 변경 동선: product/coupon/cart 관련 로직이 각각 관련 훅과 컴포넌트에 모여 있음 → 동선 짧음(응집도 높음).
    • 라이브러리로 떼어내기: hooks와 utils는 비교적 독립적. UI 컴포넌트는 Tailwind/스타일에 의존하므로 UI 패키지로 묶기 쉬움. 전체적으로 추출가능성이 높음.
  • 결합도(함수/컴포넌트 사이의 인터페이스)
    • 좋은 점: 대부분 컴포넌트는 props로 행동(함수)과 데이터를 받고 있음(예: ProductList는 addToCart, getRemainingStock를 prop으로 받음) → 낮은 결합도.
    • 개선 필요: 일부 컴포넌트(예: AdminPage에 addNotification을 그대로 내려주는 방식)에서 직접 훅을 전달하기보다 onSuccess/onError callback을 받게 하면 더 일반화되어 패키지화에 유리함.

패키지화 할 때의 TO-DO(예시)

  • 디렉토리(권장)
    • packages/
      • ui/ (ProductCard, CartList, Notification, icons) — peerDependencies: react
      • hooks/ (useCart, useProducts, useNotification) — peerDependencies: react, 선택적 상태 라이브러리
      • utils/ (validator, formatters, storage) — standalone
      • entities/ (types, pure functions like calculateCartTotal)
  • 공개 API(예)
    • hooks package export: { useCart, useProducts } — 내부 구현에 atoms/zustand/react-query를 캡슐화
    • ui package export: { ProductCard, CartList } — props 기반 API로 노출(onAdd, onRemove 등)
  1. 테스트 설계(현재 PR 관련)
  • 장점: Jotai Provider로 테스트 격리 적용(좋음).
  • 개선: 현재 테스트가 UI 문자열에 많이 의존(예: '장바구니' 텍스트, '상품1' 등). 유지보수성 향상을 위해 data-testid 혹은 role/aria-label을 적절히 활용하세요. 또한 훅들을 mocking(특히 useProducts/useCart)하면 UI 테스트를 단순화할 수 있습니다.
  1. 타입/prop interface 일부 미스매치 (코드에서 포착)
  • Notification.tsx 타입: setNotifications: (id: string) => void 로 정의했지만, 실제로는 setNotifications 또는 removeNotification 등 다양한 형태가 될 수 있음. API 이름과 시그니처가 혼재되어 있어 혼란 초래.

AS-IS vs TO-BE 예시(구체 코드 스니펫)

    1. Notification API (요약)
    • AS-IS
      const { notifications, setNotifications } = useNotification();
      <Notification notifications={notifications} setNotifications={setNotifications} />
      // Notification calls setNotifications(id) to remove
    • TO-BE
      const { notifications, addNotification, removeNotification } = useNotification();
      <Notification notifications={notifications} onDismiss={removeNotification} />
    1. useCart API: 더 안전하고 테스트 친화적으로
    • AS-IS (현재 여러 부분에서 setCart / getRemainingStock 직접 사용)
      const { cart } = useCart();
      addToCart(product); // maybe available
    • TO-BE (명확한 행동 API + callbacks)
      const useCart = () => {
        const addToCart = (product, { onSuccess, onError } = {}) => {
          if (someError) { onError?.(err); return; }
          // 성공 처리
          onSuccess?.();
        };
        return { cart, addToCart, updateQuantity, removeFromCart };
      };
      
      // 사용부
      useCart().addToCart(product, {
        onSuccess: () => addNotification('장바구니에 담겼습니다')
      });
    1. 모듈화(패키지로 떼어내기)
    • AS-IS: 프로젝트 내부에서 많은 파일들이 상대 경로로 서로 import
    • TO-BE: packages/hooks/index.ts 등으로 공용 API export, package.json에 peerDependencies 명시.

우선순위 개선 체크리스트 (권장 적용 순서)

  1. Notification API 정리: useNotification이 add/remove API를 반환하도록 변경. Notification 컴포넌트 props를 onDismiss로 바꿈. (중요/낮은 비용)
  2. storage adapter 만들기 및 usePersistentState 도입: localStorage 접근을 중앙으로 모아 응집도 증가. (중요)
  3. 훅 public API 정리: 모든 훅(useCart/useProducts/useCoupons 등)이 간결한 함수/옵션(onSuccess/onError) 기반 인터페이스를 제공하도록 리팩토링. (중요)
  4. 테스트 안정화: data-testid/role 보강 및 훅 mocking으로 UI테스트를 더 견고하게. (중요)
  5. 패키지화 준비: packages 폴더 구조 설계 및 public API 결정. (중간)
  6. 상태관리 라이브러리 교체 연습: useCart 내부 구현만 바꾸는 방식으로 순차 리팩토링 테스트 (Zustand/RTK/React Query). (중간/학습 목적)

마무리/권장 리소스

  • 좋은 개념들: “훅은 로직 재사용, 상태 공유는 전역 스토어/Context” — 이미 잘 이해하고 계십니다.
  • 추천 자료:
    • React Hooks patterns (Kent C. Dodds / EpicReact)
    • Dan Abramov의 “You Might Not Need Redux” (로컬/전역 상태 분배 관점)
    • Zustand vs Jotai vs Redux 비교 문서(프로젝트 요구사항에 따라 선택)

끝으로, PR에서 눈에 띄는 좋은 점들:

  • 도메인별 분리(components/ui, hooks, icons 등)가 잘 되어 있습니다.
  • 테스트에 Provider를 넣어 각 테스트를 격리한 점(테스트 신뢰도 상승).
  • 로컬스토리지 싱크 문제를 경험으로 해결한 점(학습의 큰 수확).

추가로 도와드릴 수 있는 것들

  • useCart/useNotification의 현재 구현(파일 내용)을 보여주시면, 더 구체적으로 TO-BE 코드를 만들어 드릴게요(예: zustand 변환, API 표준화).
  • 패키지 구조 예시(실제 파일 이동/exports)도 만들어드릴 수 있습니다.

필요하시면 우선순위 1~3 작업의 구체적 코드 변환(리팩토링 패치 형태)을 바로 만들어 드리겠습니다. 어느 부분을 우선적으로 바꿔볼까요?

Comment on lines +17 to +21
// 상품 목록 관리
const { products } = useProducts();

// 알림 관리
const { addNotification } = useNotification();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

훅 내부에서 다른 훅을 사용하는 것 보단 사용되는 컴포넌트에서 각각 호출하여 사용하는 건 어떨까요??? 아님 훅이 메세지를 리턴하도록 하는 방법도 있을 것 같아요!!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용되지 않는 파일은 푸시하기 전에 정리하면 좋을 것 같습니당

Comment on lines +15 to +23
// 고객용 포맷팅
export const formatPrice = (price: number): string => {
return `₩${price.toLocaleString()}`;
};

// 관리자용 포맷팅
export const formatPriceForAdmin = (price: number): string => {
return `${price.toLocaleString()}원`;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 포맷팅 함수가 원, ₩ 형식을 인자로 받아서 리턴하도록 해주었는데 아예 함수를 따로 만드셨네요!! 오히려 보기 편한 것 같기도 하네요


const { notifications, setNotifications } = useNotification();
const { cart } = useCart();
const [isAdmin] = useAtom(isAdminAtom);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • isAdmin은 읽기만 하므로 useAtomValue
  • totalItemCount는 쓰기만 하므로 useSetAtom

이 적절한 거 같아요!
현재처럼 useAtom의 첫 번째 값을 버리는 패턴은 의도가 흐려지는거 같습니다

const isAdmin = useAtomValue(isAdminAtom);
const setTotalItemCount = useSetAtom(totalItemCountAtom);

이렇게 바꾸면 가독성과 성능이 모두 개선될 거 같네요

Comment on lines +5 to +30
export const TabLayout = ({ activeTab, handleActiveTab }: TabLayoutProps) => {
return (
<nav className='-mb-px flex space-x-8'>
<button
onClick={() => handleActiveTab('products')}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === 'products'
? 'border-gray-900 text-gray-900'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
상품 관리
</button>
<button
onClick={() => handleActiveTab('coupons')}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === 'coupons'
? 'border-gray-900 text-gray-900'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
쿠폰 관리
</button>
</nav>
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const TabLayout = ({ activeTab, handleActiveTab }: TabLayoutProps) => {
return (
<nav className='-mb-px flex space-x-8'>
<button
onClick={() => handleActiveTab('products')}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === 'products'
? 'border-gray-900 text-gray-900'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
상품 관리
</button>
<button
onClick={() => handleActiveTab('coupons')}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === 'coupons'
? 'border-gray-900 text-gray-900'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
쿠폰 관리
</button>
</nav>
);
};
export function AdminTab({
tabs,
activeTab,
setActiveTab,
}: {
tabs: {
label: string;
value: "products" | "coupons";
}[];
activeTab: "products" | "coupons";
setActiveTab: React.Dispatch<React.SetStateAction<"products" | "coupons">>;
}) {
return (
<div className="border-b border-gray-200 mb-6">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.value}
onClick={() => setActiveTab(tab.value)}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.value
? "border-gray-900 text-gray-900"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
);
}

저는 이렇게 짰는데 이런 구조는 어떠신가요? 탭의 정보를 외부에서 받는 것이 좀 더 확장성 있는 구조 같다는 생각이 들어서요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants