eveneul 님의 상세페이지[4팀 오하늘] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 🦍

https://eveneul.github.io/front_6th_chapter2-2/basic.html

https://eveneul.github.io/front_6th_chapter2-2/advanced.html

과제의 핵심취지

  • 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는 잘 제거했나요?
  • 전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?

과제 셀프회고

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

  • 4주차 클린 코드에서 팀 컨벤션을 정해 두고, 거기에 맞는 prittier나 esLint 설정을 미리 해 두었기에 우리 페어 팀만의 컨벤션을 이번 주차에도 지킬 수 있어서 좋았습니다.

    image
  • 미리 페어 팀원분들에게 공유를 해 준 병준 님께 무한한 감사를.

  • 과제를 진행해 나가던 중 여러 트러블슈팅을 겪었습니다.

    • 베이직 과제를 진행하면서 관심사 분리를 위해 다음과 같이 구조를 나누어 접근했습니다:

      • useProduct.ts: 상품 정보를 관리하는 커스텀 훅
      • useProductForm.ts: 관리자 기능(상품 추가/삭제/수정)을 담당하는 커스텀 훅

      문제 상황과 해결 과정

      초기 문제 인식handleProductSubmit 함수에는 상품 제출 후 UI를 업데이트하는 로직(상품 관련 영역을 숨기는 기능)이 필요했습니다. 하지만 UI 업데이트 함수는 커스텀 훅에 직접 포함하기에는 적절하지 않다고 판단했습니다.

      첫 번째 시도: 콜백 함수 접근 UI 업데이트 함수를 useProductForm.ts의 콜백으로 받으려고 시도했지만 테스트를 통과하지 못했습니다.

      두 번째 시도: onSuccess 콜백과 훅 조합

      • onSuccess 함수를 콜백으로 받도록 수정
      • useProductForm.ts 내에서 useProduct 훅을 호출하여 addProduct, updateProduct 함수 사용 시도
      • 여전히 테스트 통과 실패

      커스텀 훅의 독립성 이슈

      React에서 커스텀 훅은 호출될 때마다 독립적인 상태 인스턴스를 생성하기 때문에, 여러 곳에서 같은 커스텀 훅을 호출하면 상태가 공유되지 않고 동기화 문제가 발생한다는 것을 알게 되었습니다.

    • 토스트 기능 관련 테스트가 모두 실패하는 상황이 발생했습니다.

      문제 상황과 해결 과정

      초기 문제 인식 Toast들이 생성된 후, 각각의 Toast들이 3초 뒤 없어지는 것이 아닌 모든 Toast들이 마지막으로 생긴 Toast를 기준으로 3초 뒤 없어지는 것을 확인했습니다.

      첫 번째 시도: useEffect를 통한 감지

      useEffect를 사용해 토스트가 생성되거나 삭제될 때를 감지하여 자동으로 제거하는 로직을 구현했지만 테스트를 통과하지 못했습니다.

      두 번째 시도: Toast의 ID를 조금 더 유니크(?)하게 변경

      notificationid를 Date.now()에서 Date.now() + Math.random()으로 더 고유한 값을 가지게 했습니다.

      Rect의 클로저 트랩

      useEffect의 의존성 배열이 빈 배열이어서 클로저 문제가 발생했습니다. useEffect 내부에서 외부 변수를 참조할 때, 의존성 배열에 해당 변수들을 포함하지 않으면 컴포넌트가 마운트될 때의 값만 기억하게 되어 새로운 토스트가 생성되어도 이전 토스트들은 최신 상태를 인식하지 못하고, 모든 토스트가 마지막에 생성된 타이밍에 맞춰 동작하는 문제였습니다.

      [removeNotification, id]를 의존성 배열에 추가함으로써 각 토스트가 자신만의 고유한 값들을 정확히 참조하도록 해결했습니다.

  • Jotai Provider를 main.tsx에 설정했지만, 테스트 코드는 App.tsx 컴포넌트만 확인하기 때문에 Provider 범위를 인식하지 못한다는 중요한 사실을 알게 되었습니다. Provider를 App.tsx로 옮기고, App.tsx에 있었던 기존 코드들을 AppContent.tsx라는 컴포넌트로 이동시키는 방법을 채택했습니다.

  • 과제를 본격적으로 시작하기 전, 페어 팀원분들과 엔티티 컴포넌트와 UI 컴포넌트에 대해 토론(?)을 했습니다.이론적인 이야기부터 비유를 들어 쉽게 설명하는 시간까지 가져서, 결국 UI는 Entity를 기반으로 표현되고, Entity는 UI로부터 독립적이라는 식으로 정리했습니다.

  • Jotai를 처음 사용해 봤는데, 보면 볼수록 괜찮은 친구인 것 같습니다. atomWithStorage 로 자동적으로 로컬스토리지 연동되는 게 편했습니다. Redux로 예를 들자면, Redux에서 persist 설정하던 번거로움이 없어지는 느낌이라서 마음에 듭니다. 하지만 지원하는 기능이 많아서 기능 찾느라 몇 시간은 문서만 본 것 같아요.

  • 그리고 이건 과제 내용과 상관없는 내용이기는 한데, github actions가 안 되는 이유를 찾아서 해결하고, 5주차가 되어서야 처음으로 github actions를 사용했습니다. 관련해서 벨로그 글을 작성했습니다!

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

  • UI 컴포넌트를 조금 더 확장성 있는 컴포넌트로 만들고 싶었습니다. 예를 들면 Button 같은 경우, variantsize로 여러 페이지에서 범용성 있게 사용할 수 있도록 처리했습니다.
import { ReactNode } from 'react';

interface ButtonProps {
  children: ReactNode;
  variant?: 'primary' | 'ghost' | 'link' | 'outline';
  size?: 'xs' | 'sm' | 'md' | 'lg';
  className?: string;
  onClick?: () => void;
  disabled?: boolean;
  type?: 'button' | 'submit' | 'reset';
}

export default function Button({
  children,
  type = 'button',
  variant = 'primary',
  size = 'md',
  className,
  onClick,
  disabled,
}: ButtonProps) {
  const baseClasses = '';

  const variantClasses = {
    primary: `bg-gray-900 text-white transition-colors hover:bg-gray-800 ${disabled && '!bg-gray-100 !text-gray-400 cursor-not-allowed'}`,
    ghost: 'px-3 py-1.5 text-sm rounded transition-colors text-gray-600 hover:text-gray-900',
    link: 'text-blue-600 no-underline !p-0',
    outline: 'border border-gray-300 font-medium text-gray-700 hover:bg-gray-50',
  };

  const sizeClasses = {
    xs: 'w-6 h-6 rounded flex items-center justify-center hover:bg-gray-100',
    sm: 'rounded px-3 py-1.5 text-sm',
    md: 'rounded-md px-4 py-2 text-sm',
    lg: 'py-2 px-4 rounded-md font-medium',
  };

  return (
    <button
      onClick={onClick}
      type={type}
      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
    >
      {children}
    </button>
  );
}

  • 지난 주차와 다르게(?) AI의 개입을 줄였습니다. 개인적으로 저는 디자인 시스템을 많이 만들어 봐서 Props drilling에 대해 익숙해진 상태라, AI의 많은 도움 없이 과제를 진행할 수 있었습니다.

  • 유틸리티 함수 구조화를 통해 동일한 계산 로직이지만 폴더 구조를 조금 더 체계적으로 정리했습니다.

    image

    utils 폴더 안에 calculationsformatters 폴더를 나누었고, calculations 같은 경우에는 각 목적마다 쓰이는 계산 함수를 정리했습니다.

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

  • 테오 코치님이 만들어 주신 hint를 보지 않고 진행해 나갔는데, 다음에는 useLocalstorage.ts나 useDebounce.ts 같은 훅을 더 분리해 보고 싶습니다.

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

  • 제가 컴포넌트를 크게 쪼갰을까요?

    • 컴포넌트가 몇 줄 이상 넘어가면 안 된다! 라는 게 있으신가요? 예를 들어 한 컴포넌트가 150줄 이상 넘어가면, 그 안에서도 어떻게든 컴포넌트로 뺄 것을 찾는다든지요. 예를 들어 ProductForm.tsx에서 form 안에 있는 Input들과 그에 대한 로직들이 많아서 코드가 생각보다 긴 편(?)입니다. 여기에서도 컴포넌트를 따로 분리를 해 주어야 하는 걸까요?
  • Jotai vs Zustand

    • 이번에 Jotai를 사용했는데, 실제 업무에서는 어떤 상태 관리 라이브러리를 선호하시나요? 프로젝트 규모나 팀 상황에 따라 달라지는지 궁금합니다.

과제 피드백

고생하셨습니다 하늘님 ㅎㅎ 이번 과제를 진행하면서 배우셨던 많은 내용들을 글로도 작성해주시고 다른 분들께 공유도 해주신 점 너무 멋지네요. 공통 컴포넌트를 확장성 있는 컴포넌트로 구성하기 위해 공통 컴포넌트를 작성하신 경험도 나눠주신것도 좋고 AI 개입을 줄이고 직접 작성해보신 것도 오히려 과제의 퀄리티를 높이는데 좋은 영향이 있었던 것 같네요 ㅎㅎ

질문 주신 것 답변 하고 마무리 해볼게요

제가 컴포넌트를 크게 쪼갰을까요?

절대적인 코드라인 수 기준을 만드는건 개인적으로는 선호하지 않는 것 같아요. 여기서 저희가 말하는 일반적으로 강조하는 기준들이 등장하는데요. 가독성, 재사용성, 유지보수성 뭐 이런것들이요! 이런것들을 지키기 위해서는 단일 책임원칙을 지켜서 하나의 컴포넌트에서 하나의 책임을 명확하게 갖도록 하는게 이상적이죠. 지금의 컴포넌트에서는 Input들에 들어가는 여러 로직들이 많이 복잡한데(가공을 해서 넣는다라거나..) 이런 부분들을 분리해서 관리해보고 그리고 컴포넌트 관점에서 공통적으로 분리할 수 있는, 재사용 할 수 있는 컴포넌트는 없는지 고민해봐도 좋을것 같아요 ㅎㅎ

Jotai vs Zustand

저는 둘 다 사용해야 하는 사례에 따라 선택하는 편인데, 사실 크게 중요했던적은 없었던 것 같아요 ㅎㅎ 둘의 핵심차이는 사실 철학차이이고 상태를 정교하게 관리하는 것이 필요하냐 안하냐의 차이인것 같아요. 하지만, 요즘의 서버상태와 UI상태를 분리하고 가벼운 상태만 전역 상태로 관리하려는 추세에서는 둘의 구성이 크게 차이가 없고 무리가 되는 부분이 없기 때문에 무난하게 선택해도 상관없을 것 같아요. 다만, 위에서 언급한것처럼 둘의 핵심차이는 철학 차이이니까 해당 부분을 공부해보는건 필요할것 같아요!

고생하셨고 다음주도 화이팅입니다~