Skip to content

Conversation

@ds92ko
Copy link

@ds92ko ds92ko commented Dec 1, 2025

배포 링크

과제의 핵심취지

  • 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는 분리되어 있나요?

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

심화과제

  • 이번 심화과제는 Context나 Jotai를 사용해서 Props drilling을 없애는 것입니다.

  • 어떤 props는 남겨야 하는지, 어떤 props는 제거해야 하는지에 대한 기준을 세워보세요.

  • Context나 Jotai를 사용하여 상태를 관리하는 방법을 익히고, 이를 통해 컴포넌트 간의 데이터 전달을 효율적으로 처리할 수 있습니다.

  • Context나 Jotai를 사용해서 전역상태관리를 구축했나요?

  • 전역상태관리를 통해 domain custom hook을 적절하게 리팩토링 했나요?

  • 도메인 컴포넌트에 도메인 props는 남기고 props drilling을 유발하는 불필요한 props는 잘 제거했나요?

  • 전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?

과제 셀프회고

이번 과제를 시작하기 전까지 저는 함수형 프로그래밍(FP)에 대해 다음과 같은 오해를 가지고 있었습니다.

  • 오해 1: FP는 OOP와 상반되는 개념이다.
    FP는 객체의 상태 변경을 지양하고 순수 함수와 불변성에 중점을 두기 때문에, OOP의 핵심인 캡슐화와 Mutation을 부정한다고 생각했습니다.

  • 오해 2: FP는 모든 것을 순수 함수로만 만들어야 한다.
    실제 애플리케이션의 부수 효과(Side Effect), 즉 네트워크 통신, 상태 변경, 콘솔 출력 같은 필수적인 동작을 FP가 어떻게 다룰 수 있는지에 대한 이해가 부족했습니다.

  • 오해 3: FP는 실무의 복잡한 비즈니스 로직을 구현하기에는 너무 이론적이다.
    단순한 map, filter, reduce 이상의 복잡한 로직을 FP 방식으로 구조화하는 것이 비효율적이라고 생각했습니다.

하지만 이번 과제를 통해 FP의 목표와 실용적 가치를 깊이 이해하게 되었습니다.

  • FP의 목표는 OOP의 대안이 아닌 보완
    FP는 OOP를 대체하는 것이 아니라, "어떻게 하면 더 안전하게 데이터를 다룰 수 있을까?" 에 초점을 맞춥니다.

    • OOP의 장점: Product 엔티티처럼 데이터와 관련된 행위(메서드)를 캡슐화하여 도메인의 관심사를 효과적으로 분리합니다.
    • FP의 역할: 장바구니 총액 계산 로직(calculateCartTotal)처럼 데이터를 변경하지 않는 순수한 계산은 컴포넌트나 객체의 내부 상태에 묶어두지 않고, 순수 함수(Pure Function)로 분리하여 예측 불가능한 부수 효과를 제거하는 데 활용됩니다.

    결국, 이번 과제에서 저는 OOP가 담당해야 할 엔티티 캡슐화와 FP가 담당해야 할 계산의 순수성을 models/ 폴더 분리를 통해 동시에 확보할 수 있었습니다.

  • 핵심은 순수 함수 최대화와 부수 효과 캡슐화
    FP는 모든 것을 순수하게 만들라는 것이 아니라, 순수 함수를 최대한 늘리고, 부수 효과(Side Effect)는 의도된 경계에 모아 관리하라는 철학임을 깨달았습니다.

    • Calculation (순수): calculateCartTotal과 같은 계산 로직은 models/에 순수 함수로 분리되었습니다.
    • Action (부수 효과): 상태를 변경하거나 알림을 표시하는 addToCarthandleNotificationAdd 같은 함수는 Custom Hook이나 Zustand Store의 Action에 캡슐화하여 부수 효과가 발생하는 지점을 명확히 통제했습니다.

    이러한 Action / Calculation / Data 분리를 통해, 순수하지 않은 코드는 격리시키고 핵심 비즈니스 로직은 예측 가능하게 만들 수 있었습니다.

  • FP는 복잡성 관리의 강력한 도구
    복잡한 할인 계산 로직을 getMaxDiscountRate, hasBulkPurchase 등 작은 순수 함수들로 쪼개고 이를 조합하는 함수 합성 방식을 적용해보니, 복잡한 로직을 마치 레고 블록처럼 쉽게 이해하고 테스트하며 확장할 수 있었습니다.

    로직이 변경되더라도 전체 시스템이 아닌 특정 작은 함수 블록만 수정하면 되므로, 유지보수성과 확장성 측면에서 FP가 실무에 얼마나 실용적인 아키텍처 원칙인지 깊이 체감할 수 있었습니다.

과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?

기본 과제

초기 1,123줄의 App.tsx 코드를 리팩토링하면서, 단순히 코드를 분할하는 것을 넘어 관심사 분리와 FP 사고를 적용하는 것의 가치를 체감했습니다.

  • 함수형 프로그래밍 원칙 적용

    기존 코드에서는 calculateCartTotal() 같은 함수가 컴포넌트 내부 변수를 클로저로 참조하여 외부 상태에 의존했습니다.
    이는 함수형 프로그래밍의 핵심 원칙인 순수 함수(Pure Function)를 위반하는 패턴이었습니다.

    순수 함수와 부수 효과 제거:

    // origin: 부수 효과가 있는 함수 (외부 상태 의존)
    const calculateCartTotal = () => {
      // 컴포넌트 내부 변수 cart, selectedCoupon을 클로저로 참조
      return cart.reduce(...) + selectedCoupon.discount;
    };
    
    // advanced: 순수 함수 (명시적 파라미터, 부수 효과 없음)
    export const calculateCartTotal = (cart: CartItem[], selectedCoupon: Coupon | null) => {
      // 같은 입력에 대해 항상 같은 출력 보장
      return cart.reduce(...) + (selectedCoupon?.discount || 0);
    };

    그 외에도

    불변성을 통한 안전한 데이터 조작:
    filter, map과 같은 배열 메서드를 사용해 새 배열을 반환하거나, 스프레드 연산자로 새 배열을 반환하도록 처리했습니다.

    함수 합성으로 복잡한 로직 구현:
    작은 순수 함수들을 합성하여 복잡한 계산을 수행하도록 처리했습니다.

    고차 함수와 선언적 프로그래밍: 기존의 명령형 방식을 선언적 방식으로 리팩토링

    // origin: 명령형 패턴 (forEach + 변수 누적)
    const calculateCartTotal = (): {
      totalBeforeDiscount: number;
      totalAfterDiscount: number;
    } => {
      let totalBeforeDiscount = 0; // 변수 선언 및 초기화
      let totalAfterDiscount = 0;
    
      cart.forEach(item => {
        // forEach로 반복하면서 변수 변경
        const itemPrice = item.product.price * item.quantity;
        totalBeforeDiscount += itemPrice; // 부수 효과: 변수 변경
        totalAfterDiscount += calculateItemTotal(item);
      });
      // ...
    };
    
    // advanced: 선언형 패턴 (reduce로 누적)
    export const calculateCartTotal = (cart: CartItem[], selectedCoupon: Coupon | null) => {
      const { totalBeforeDiscount, totalAfterDiscount: beforeCoupon } = cart.reduce(
        (acc, item) => ({
          totalBeforeDiscount: acc.totalBeforeDiscount + item.product.price * item.quantity,
          totalAfterDiscount: acc.totalAfterDiscount + calculateItemTotal(item, cart)
        }),
        { totalBeforeDiscount: 0, totalAfterDiscount: 0 } // 초기값
      );
      // ...
    };

    이렇게 변경하면서 함수형 프로그래밍의 핵심 원칙들을 적용할 수 있었습니다:

    • 순수성(Purity): 같은 입력에 대해 항상 같은 출력을 보장
    • 명시적 의존성(Explicit Dependencies): 모든 의존성을 파라미터로 명시
    • 부수 효과 제거(Side Effect Free): 외부 상태를 변경하지 않음
    • 불변성(Immutability): 원본 데이터를 변경하지 않고 새로운 데이터를 반환
    • 함수 합성(Function Composition): 작은 순수 함수들을 조합하여 복잡한 로직 구현

    덕분에 테스트 코드 작성과 버그를 발견하고 예방하기 용이한 코드로 리팩토링이 가능했습니다.
    또한 이 로직을 다른 컴포넌트나 모듈에서 재사용할 수 있어 유지보수와 확장성 측면에서도 큰 도움이 됐습니다.

  • 코드 구조 개선과 관심사 분리를 통한 유지보수성 극대화

    초기 App.tsx는 1,123줄에 달하며 모든 로직이 한 곳에 섞여 있었습니다.
    이는 디버깅과 기능 수정 등에 엄청난 비효율을 발생시키게 됩니다.

    해결 방법:

    • 기능별로 작게 분리하고 각 모듈에 단일 책임 원칙 적용
    • Custom Hook을 도입해 상태관리 및 비즈니스 로직 캡슐화
    • 순수 함수를 models/(도메인 로직)와 utils/(범용 유틸리티) 폴더로 분리하여 부수 효과 없는 계산 로직 관리

    이를 적용하자, 코드를 읽고 이해하는 시간이 획기적으로 단축되었으며, 컴포넌트는 오직 View 렌더링 역할에 집중하게 되었습니다.
    특히 함수형 프로그래밍 원칙에 따라 순수 함수로 분리된 계산 로직은 예측 가능하고 테스트하기 쉬워졌습니다.
    이를 통해 모듈화된 아키텍처와 함수형 프로그래밍이 대규모 애플리케이션의 유지보수성과 협업 효율에 얼마나 필수적인 요소인지 깊이 체감했습니다.

심화과제

상태 관리 라이브러리(Zustand)와 영속화(Persist) 기능을 적용하며 전역 상태 관리의 심화 패턴을 학습했습니다.

  • Zustand 전역 상태의 특성 이해

    useNotifications를 전역 상태로 변경하는 과정에서 이전 테스트의 알림이 다음 테스트에 영향을 주며 기존 테스트가 깨지는 문제가 발생했습니다.
    useState는 렌더링마다 새로운 상태를 생성하므로 상태가 독립적이지만, Zustand store는 전역 싱글톤으로 메모리에 유지됩니다.
    이 때문에 테스트 환경에서 이전 테스트의 데이터가 다음 테스트에 남아 예상치 못한 오류를 일으킬 수 있음을 알게 되었습니다.
    전역 상태 관리 환경에서는 테스트 간 상태 간섭을 막기 위해 명시적 초기화가 필수적이라는 점을 체득했습니다.

  • persist 미들웨어 동작 원리 파악

    persist를 적용하면서, persist store가 스토어 생성 시 localStorage에서 상태를 읽어와 초기 상태로 복원하기 때문에 단순히 localStorage를 지우는 것만으로는 충분하지 않고, 스토어 상태와 localStorage를 동시에 초기화해야 함을 알게 되었습니다.

    beforeEach(() => {
        // localStorage 초기화
        localStorage.clear();
    +    // store 초기화
    +    useCart.getState().actions.clearCart();
        ...
    });
    • 테스트에서 localStorage를 지워도 이미 메모리에 존재하는 store 인스턴스에는 이전 상태가 그대로 남아 있습니다.
    • persist store는 localStorage를 읽어 스토어 상태를 초기화하는 시점이 스토어 생성 시점이므로, localStorage만 초기화하면 이전 데이터가 유지됩니다.

    이를 통해 persist store가 localStorage와 메모리 상태를 어떻게 복원하고 동기화하는지 직접 경험하며, 전역 상태 관리 + 영속화(store persist) 동작 원리를 이해할 수 있었습니다.


이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?

기본과제

  • PageType 기반 설계로 확장성 확보

    기존에는 isAdmin 같은 boolean 플래그로 페이지를 구분했기 때문에 페이지가 늘어날수록 플래그와 분기문이 함께 증가하는 구조적 문제가 있었습니다.

    const [isAdmin, setIsAdmin] = useState(false);

    예를 들어 마이페이지가 추가되면 다음과 같이 새로운 플래그와 조건이 계속 붙습니다.

    const [isAdmin, setIsAdmin] = useState(false);
    + const [isMyPage, setIsMyPage] = useState(false);
    
    if (isAdmin) {...}
    + else if (isMyPage) {...}

    이는 다음과 같은 문제를 만들어 냅니다.

    • 상태가 늘어날수록 컴포넌트 복잡도 증가
    • 누락·충돌 같은 버그 가능성 증가
    • 타입 시스템에서 페이지 종류를 제어할 수 없음
    • 페이지가 많아질수록 유지보수 비용이 선형적으로 증가

    이 문제를 해결하기 위해 페이지 구분 방식을 boolean 기반이 아닌 PageType 기반의 단일 책임 구조로 전환했습니다.

    export const PAGES = {
      store: 'store',
      admin: 'admin'
    } as const;
    
    const usePage = (initialPage: PageType = PAGES.store) => {
      const [currentPage, setCurrentPage] = useState<PageType>(initialPage);
    
      const switchPage = useCallback((page: PageType) => {
        setCurrentPage(page);
      }, []);
    
      const isCurrentPage = useCallback((page: PageType) => currentPage === page, [currentPage]);
    
      return {
        currentPage,
        setCurrentPage,
        switchPage,
        isCurrentPage
      };
    };

    이 방식으로 페이지를 관리할 경우, 새로운 페이지를 추가할 때 PAGES만 확장하면 되고 기존 로직은 수정할 필요가 없습니다.

    export const PAGES = {
      store: 'store',
      admin: 'admin',
    + mypage: 'mypage'
    } as const;

    확장성, 가독성, 타입 안정성 모두 확보할 수 있는 구조로 개선했습니다.

  • Compound Component 패턴과 Context 구성

    Tabs UI는 여러 하위 컴포넌트가 상호작용해야 하는 구조입니다.

    • Tabs (root)
    • TabList
    • Tab
    • TabPanel

    이런 구조는 Compound Component 패턴의 장점을 그대로 가져갈 수 있는 대표적인 케이스입니다.

    <Tabs>
      <TabList>
        <Tab>...</Tab>
      </TabList>
      <TabPanel>...</TabPanel>
    </Tabs>

    이 패턴을 선택한 이유는 다음과 같습니다

    • 선언적 UI 구성
      사용자가 <Tabs> 컴포넌트를 구성할 때, "어떻게 동작하는지"가 아니라 "어떤 UI가 필요한지"만 표현하면 됩니다.

    • 내부 통신 구조 단일화
      TabList, TabPanel은 서로 다른 레벨의 컴포넌트이지만 하나의 Tabs 상태를 공유해야 합니다.
      이를 위해 private Context를 사용했고, 이는 Tabs 내부에서만 쓰이기 때문에 분리하지 않았습니다.

      → 응집도가 높아지고, 외부 노출도 방지되며 책임 범위가 명확해집니다.

    • 로직의 재사용성 확보
      상태 제어 로직은 UI와 분리할 필요가 있었기 때문에 useTabs를 별도 hook으로 만들어 범용적으로 활용할 수 있게 했습니다.

  • 아이콘 네이밍 기준 결정

    Icon 컴포넌트를 추출하면서 네이밍을 용도 기반(close, delete, add 등)으로 할지 모양 기반(x, trash, plus 등)으로 할지 고민했습니다.

    용도 기반 네이밍의 장점과 한계:

    용도 기반 네이밍을 적용하면 아이콘 이름이 명확해집니다.

    • CloseIcon
    • DeleteIcon
    • AddIcon

    이 방식은 어떤 UI 동작을 위한 아이콘인지 직관적으로 이해할 수 있다는 장점이 있습니다.
    하지만 같은 모양의 아이콘을 다른 용도로 사용할 때 아이콘 컴포넌트가 중복 생성되는 문제가 발생합니다.

    예를 들어 X 모양 아이콘을 생각해보면,

    • 모달 닫기 버튼 → CloseIcon
    • 장바구니 아이템 삭제 버튼 → DeleteIcon

    둘다 동일한 X 모양을 사용하지만, 용도가 다르다는 이유만으로 서로 다른 아이콘 컴포넌트를 만들어야 합니다.
    이렇게 되면 용도가 변경되거나 새로운 용도가 추가될 때마다 아이콘 컴포넌트를 새로 만들거나 이름을 변경하는 비효율이 생깁니다.
    이 문제를 해결하기 위해 모양 기반 네이밍(XIcon, TrashIcon, PlusIcon 등)으로 방향을 정했습니다.

    모양 기반 네이밍의 장점:

    • 하나의 아이콘을 다양한 용도에서 재사용 가능
    • 용도가 바뀌어도 컴포넌트 이름을 다시 만들거나 수정할 필요 없음
    • 아이콘의 그래픽 형태 기준으로 관리되기 때문에 일관성 유지가 쉬움
    • UI 전체에서 시각적 구성 요소를 기준으로 관리할 수 있어 디자인 시스템 관점에서도 더 안정적

    X 모양 자체는 변하지 않기 때문에, 용도가 달라져도 컴포넌트를 추가로 만들 필요가 없습니다.
    이런 점을 종합해볼 때 재사용성·유연성·유지보수성 관점에서 모양 기반 네이밍이 더 적합하다고 판단했습니다.

심화과제

  • Zustand Persist 호환성 문제 해결을 위한 커스텀 Storage 구현

    Zustand persist를 적용하면서 아래와 같은 저장 구조 때문에 문제가 발생했습니다.

    {
      state: {...}, 
      version: 0
    }

    advanced 버전은 이 구조를 저장하지만, basic 버전은 배열만 저장하는 단순 구조였습니다.
    → 즉, 기본 저장 구조가 달라서 기존 데이터를 읽지 못하는 호환성 문제가 생김

    이를 해결하기 위해 처음에는 partialize로 필요한 부분만 저장하도록 시도했습니다.

    {
    - partialize: ({ context }) => ({ context })
    + partialize: ({ context }) => [...context.cart]
    }

    하지만 persist의 기본 포맷을 바꿀 수 없었기 때문에, 최종적으로 직접 PersistStorage를 구현해 advanced 버전에서도 basic과 동일한 구조로 저장되도록 만들었습니다.

    export const cartStorage: PersistStorage<{ context: CartContext }> = {
      getItem: name => {
        const stored = localStorage.getItem(name);
        if (!stored) return null;
    
        return JSON.parse(stored);
      },
      setItem: (name, value) => {
        const cart = value.state.context.cart;
        localStorage.setItem(name, JSON.stringify(cart));
      },
      removeItem: name => {
        localStorage.removeItem(name);
      }
    };

    이 방식으로 basic 버전과 동일한 localStorage 구조를 유지하면서도 zustand persist의 자동 저장·복원 기능을 그대로 활용할 수 있었습니다.

  • searchTerm 전역 상태 관리 결정

    searchTerm은 Header의 검색 인풋과 StorePage의 ProductList처럼 서로 다른 서브트리 컴포넌트들이 공유하는 값입니다.

    App
    ├── Header
    │   └── Input
    │
    └── StorePage
        └── main
            └── StorePage
                └── ProductSection
                    └── ProductList
    

    이를 props로 전달하려면 App까지 거슬러 올라가는 구조가 되는데, 검색 기능은 App의 책임이 아니기 때문에 비효율적이고 자연스럽지 않다고 판단했습니다.
    또한 두 컴포넌트는 서로 내부 동작을 몰라도 되는 독립된 모듈입니다.

    • Header/Input → 입력 UI
    • StorePage/ProductList → 필터링된 상품 리스트

    하지만 동일한 검색 상태를 공유해야 하므로 전역 상태가 더 적합하다고 판단했습니다.
    그래서 zustand store로 분리해 아래 이점을 확보했습니다.

    • props drilling 완전 제거
    • 검색 기능이 필요한 컴포넌트만 직접 구독
    • 컴포넌트 결합도 감소
    • 검색 로직의 단일 책임 확보

이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!

이번 과제에서 관심사 분리와 모듈화의 중요성을 깊이 체감했습니다.
이를 바탕으로 프로젝트의 확장성과 유지보수성을 극대화하기 위해, 코드 구조를 한 단계 더 발전시키는 것에 도전해보고 싶습니다.

기능 중심의 코드 구조 개선

현재 프로젝트는 models/, hooks/, components/, pages/, stores/, storage/ 등 역할 기반으로 코드가 분산되어 있습니다.

현재 구조 예시 (장바구니 기능):
src/advanced/
├── models/cart.ts          (계산 로직)
├── stores/cart.ts          (전역 상태)
├── storage/cart.ts         (persist storage)
├── hooks/selected-coupon.ts (로컬 상태)
└── pages/store/components/cart-section.tsx (UI)

이러한 구조는 특정 기능을 수정하거나 확장할 때 관련 코드를 여러 폴더에서 찾아야 하는 비효율성과 기능의 전체 맥락 파악의 어려움을 야기했습니다.

따라서 다음 프로젝트에서는 기능 중심으로 코드를 설계하는 FSD 아키텍처를 도입해보고 싶습니다.
FSD 구조를 적용하면 특정 기능(Feature)에 관련된 모든 코드(모델, 상태, UI, 로직)가 하나의 디렉토리 내에 모여 응집도가 극대화됩니다.

FSD 구조 예시:
src/advanced/
├── features/
│   ├── cart/                   // 장바구니 관련 모든 코드
│   │   ├── models/
│   │   ├── store.ts
│   │   └── components/
│   ├── products/
│   └── coupons/
└── shared/                     // 공통 유틸리티 및 컴포넌트
  • 높은 응집도: 기능 단위로 코드가 독립적으로 관리되어 개발 및 테스트가 용이해집니다.
  • 명확한 경계: 새로운 기능 추가 시 코드를 작성해야 할 위치가 명확해져 온보딩 및 확장성이 향상됩니다.
  • 유지보수성: 기능 단위로 코드를 추적하고 수정하기 쉬워져 장기적인 프로젝트의 유지보수 효율을 높일 수 있을 것이라 기대합니다.

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

searchTerm 전역 상태 관리 결정에 대한 질문

  • searchTerm을 전역 상태로 분리하여 관리한 아키텍처적 결정에 대해 리뷰를 받고 싶습니다.

    searchTerm은 서로 다른 컴포넌트 서브트리(검색 인풋, 상품 목록 등)에서 동시에 참조하고 변경해야 하는 값이었습니다. 이 값을 Props Drilling을 통해 전달할 경우 최상위 App 컴포넌트가 검색이라는 특정 기능의 책임까지 떠맡게 되어 관심사 분리 원칙에 위배된다고 판단했습니다.

    따라서 두 모듈이 동일한 검색 상태에 직접 접근하도록 전역 상태 관리 방식을 채택하는 것이 더 적절하다고 판단했는데, 이 선택이 해당 상태의 특성을 고려했을 때 적합한 결정이었을까요?

Compound Component 패턴과 Context 설계에 대한 질문

  • 제네릭 타입 T를 Provider에 전달해야 하는 Compound Component 패턴을 구현하면서, Context 생성 시점에 any를 사용하고 useContext를 사용하는 훅에서 타입 단언을 수행하여 타입을 지정하는 방식이 타입 안전성 측면에서 적절한 해결책인지 궁금합니다.

    const TabsContext = createContext<TabsContextValue<any> | null>(null);
    
    const useTabsContext = <T extends string>() => {
      const context = useContext(TabsContext);
      return context as TabsContextValue<T>; // 타입 단언
    };

Props 유지 기준에 대한 질문

  • totals와 같이 장바구니 상태를 기반으로 계산되는 결과값을 처리할 때, 다음 두 방식 중 어떤 것이 더 나은 아키텍처적 선택일까요?
    현재 방식처럼 부모 컴포넌트(예: StorePage)에서 계산 후 Props로 자식 컴포넌트(예: CouponSection, PaymentSection)에 전달하는 방식과 자식 컴포넌트가 Store에서 필요한 상태를 구독하여 각 컴포넌트 내부에서 계산을 수행하는 방식 중 어떤 방식이 더 적절한지 의견을 묻고 듣고싶습니다.

    // 현재 방식: StorePage에서 계산 후 props로 전달
    const StorePage = () => {
      const { cart } = cartContext();
      const [selectedCoupon, setSelectedCoupon] = useSelectedCoupon();
      const totals = calculateCartTotal(cart, selectedCoupon); // 계산 결과
    
      return (
        <CouponSection totals={totals} selectedCoupon={selectedCoupon} />
        <PaymentSection totals={totals} />
      );
    };
  • selectedCoupon을 로컬 상태로 두는 것과 전역 상태로 관리하는 것의 기준이 무엇일까요?
    현재 selectedCouponStorePage 내에서만 사용하는 로컬 상태로 관리하고 있습니다.
    이 결정에 대해 다음과 같은 의문이 있습니다.

    • 판단 근거의 적절성: selectedCouponStorePage 내부에서만 사용되고 있어 로컬 상태를 유지했는데, 이 판단이 적절했을까요?
    • 상태 관리 기준: 상태를 로컬로 둘지 전역으로 관리할지 판단하는 명확한 기준은 무엇일까요? (사용 범위, 관심사 분리, 변경 빈도, Props Drilling의 깊이 등을 고려했을 때의 최적의 기준에 대한 조언을 구합니다.)
    • 만약 selectedCoupon을 전역 상태로 분리해야 한다면, 프로젝트의 주요 전역 상태 관리 툴인 Zustand를 사용하는 것이 좋을까요, 아니면 StorePage 컴포넌트 서브트리 내에서만 사용하도록 Context API를 활용하여 부분적인 전역 상태로 관리하는 것이 더 적절한 선택일까요?

Zustand에 대한 질문

  • Zustand selector를 사용하여 상태 객체 전체를 구독하는 대신, 필요한 속성(cart, totalItemCount 등)만 더 세밀하게 구독하는 방식이 실제 렌더링 성능 최적화에 유의미한 이점을 가져다줄까요?

    // 예시: 더 세밀한 selector 사용
    const cart = useCart(state => state.context.cart); // cart만 구독
    const totalItemCount = useCart(state => state.context.totalItemCount); // totalItemCount만 구독
  • 현재 cartContext(), cartActions() 와 같이 특정 객체를 반환하는 Selector 함수 패턴을 사용하고 있습니다.
    이 방식이 Zustand Store에서 context나 actions를 분리하여 사용하는 데 있어 성능(리렌더링) 측면에서 이점이 있는지 궁금합니다.

    // 현재 패턴
    export const cartContext = () => useCart(({ context }) => context);
    export const cartActions = () => useCart(({ actions }) => actions);
  • Basic 버전과의 호환성을 위해 setItem시 배열만 저장하도록 커스텀 Storage를 구현했습니다.
    이러한 버전/호환성 문제가 발생했을 때, 커스텀 Storage 구현 외에 다른 아키텍처적 대안이 있을까요?
    이 경우, 오히려 Zustand의 persist 미들웨어를 사용하지 않고 수동으로 localStorage를 관리하는 것이 더 나은 선택이었을까요?

    // 현재 커스텀 Storage 구현
    export const cartStorage: PersistStorage<{ context: CartContext }> = {
      getItem: name => {
        const stored = localStorage.getItem(name);
        if (!stored) return null;
        return JSON.parse(stored);
      },
      setItem: (name, value) => {
        const cart = value.state.context.cart;
        localStorage.setItem(name, JSON.stringify(cart)); // 배열만 저장
      },
      removeItem: name => {
        localStorage.removeItem(name);
      }
    };

아이콘 네이밍 기준에 대한 질문

아이콘 네이밍을 결정하는 데 있어 재사용성과 유연성을 고려하여 모양 기반 네이밍(예: XIcon, TrashIcon)을 선택했습니다.
하지만 이는 용도 기반 네이밍(예: CloseIcon, DeleteIcon)과 비교했을 때 다음과 같은 의문이 발생했습니다.

  • 디자인 시스템 관점: 장기적인 유지보수와 일관성 측면에서 모양 기반과 용도 기반 중 어떤 네이밍 방식이 더 적합한 선택일까요?

  • 유연성: 모양 기반 네이밍이 재사용성에 유리하다고 판단했으나, 만약 아이콘의 모양 자체가 변경될 경우 네이밍도 함께 수정해야 하는 단점이 발생합니다. 이를 회피할 수 있는, 모양 기반 및 용도 기반 외의 더 나은 네이밍 기준이나 접근 방식이 있을까요?

질문이 많아 죄송합니다! 그리고 감사합니다!

ds92ko added 30 commits December 2, 2025 00:47
- isAdmin과 같은 단일 boolean 플래그 -> pageType 기반 설계로 변경
- showForm을 위한 useToggle 커스텀훅 추가
- useForm 커스텀훅 추가
- 쿠폰 관련 타입 및 상수 추가
- 직관적인 네이밍을 위해 constraints -> validation rules로 변경
- 확장성을 위해 discount -> coupon으로 변경
ds92ko added 30 commits December 4, 2025 19:32
- CartSection: 사용하지 않는 addNotification props 제거
- ProductSection: 사용하지 않는 addNotification props 제거
- StorePage: 불필요한 props 전달 제거
- Props Drilling 감소 및 코드 간결성 향상
- README 원칙 준수: 실용적 판단력을 통한 개선
- CartSection의 map 내부 로직을 CartItem 컴포넌트로 분리
- 각 컴포넌트의 책임 명확화 및 가독성 향상
- README 원칙 준수: 적절한 컴포넌트 분리
- CartItem 타입 import 경로를 ../../../../types에서 ../../../types/carts로 변경
- import 순서 정리로 가독성 향상
- 코드 일관성 유지
- hasCartItems를 totalItemCount > 0로 대체하여 중복 제거
- 불필요한 useMemo 제거 및 import 정리
- 코드 간결성 향상
- CartItem에서 직접 calculateItemTotal을 import하여 사용
- CartSection의 calculateItemTotal prop 제거로 Props Drilling 감소
- StorePage의 불필요한 useMemo 제거
- README 원칙 준수: Props Drilling 감소 및 컴포넌트 자율성 향상
- handleSwitchToAdmin과 handleSwitchToStore의 의존성 배열에서 상수 제거
- admin과 store는 상수이므로 의존성 배열에 포함할 필요 없음
- 코드 품질 향상
- StorePage와 CartSection에 totalItemCount prop 추가
- cart.length > 0을 totalItemCount > 0로 변경
- cart.length === 0을 totalItemCount === 0로 변경
- useCart에서 제공하는 totalItemCount 활용으로 일관성 향상
- README 원칙 준수: 단일 책임 원칙 및 데이터 일관성
- 컴포넌트 내부에서만 사용되는 이벤트 핸들러의 useCallback 제거
- useCallback 사용 기준 명확화:
  * 다른 hook의 dependency array에 포함되는 경우만 유지
  * 자식 컴포넌트에 전달되지만 React.memo로 최적화되지 않은 경우 제거
- 컴포넌트 내부 이벤트 핸들러의 useCallback 제거
- useCallback은 다른 hook의 dependency나 외부 노출 함수에만 사용
- useEffect 의존성 배열 최적화 (안정적인 함수 제외)
- CouponForm의 options와 App의 nav/page 객체 최적화
- 상태 업데이트를 함수형 패턴으로 변경 (setForm)
- calculateCartTotal을 reduce로 리팩토링
- 불필요한 props 제거
- Coupon, Product 관련 함수들을 utils에서 models로 이동
- 빈 utils 파일들 삭제
- 엔티티를 다루는 함수는 모두 models로 분리 완료
심화과제 작업을 위해 basic 폴더의 리팩토링된 코드를 advanced 폴더로 복사
- useState 기반 로컬 상태에서 Zustand store로 전환
- Props drilling 제거, 각 컴포넌트가 직접 store 사용
- Toast 컴포넌트가 store에서 직접 notifications 구독
- useLocalStorage 기반 useCart hook → zustand persist 스토어로 교체
- 전역 상태 관리로 Props drilling 제거
- useLocalStorage 기반 useProducts hook → zustand persist 스토어로 교체
- 전역 상태 관리로 Props drilling 완전 제거
- useLocalStorage 기반 useCoupons hook → zustand persist 스토어로 교체
- 전역 상태 관리로 Props drilling 완전 제거
- discountTypeOptions useMemo 제거 (상수 기반 계산이므로 불필요)
zustand persist 기본 구조 대신 배열만 저장하도록 커스텀 PersistStorage 구현하여
basic 버전과 동일한 localStorage 구조로 통일
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.

1 participant