Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] [6팀 소수지] [Chapter 2-2] 디자인 패턴과 함수형 프로그래밍 #2

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

devsuzy
Copy link

@devsuzy devsuzy commented Jan 13, 2025

과제 체크포인트

기본과제

  • React의 hook 이해하기

  • 함수형 프로그래밍에 대한 이해

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

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

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

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

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

심화과제

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

  • 엔티티 -> 리파지토리 -> 유즈케이스 -> UI 계층에 대한 이해

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

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

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

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

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

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

과제 셀프회고

과제에서 좋았던 부분

🏊‍♀️ Deep in 함수형 프로그래밍

1. 순수함수

  • 코드를 크게 액션, 계산, 데이터로 분리합니다.
  • 액션과 계산을 확실히 분리해서 액션을 최소화하고 계산함수를 만들어 관리합니다.
  • 계산 함수는 명시적인 입출력마 가지며 어떠한 부수효과도 만들어 내지 않습니다.

2. 불변성

  • 계산 함수는 여러 번 실행해도 외부의 영향에 값을 변경하지 않아야 합니다.
  • 자바스크립트는 객체나 배열과 같은 값을 다룰 때 원본을 그대로 전달하고 직접 수정할 수 있는 방법을 사용합니다. -> pass by reference
  • 함수에서 원본값을 직접 수정한다면 메모리상으론 효율적이지만 외부에 영향을 끼치게 됩니다.
  • 배열이나 객체의 값을 직접 조작하지 않고 값을 복사 후 수정하여 원본을 건드리지 않는 방법을 사용합니다. -> pass by value
  • 원본의 값을 복사하여 수정하는 카피 온 라이트 방법을 통해 액션을 계산으로 만들 수 있습니다. -> 얕은 복사
  • 만약 액션을 직접 수정할 수 없다면 방어적 복사 기법을 통해 중첩된 모든 구조를 복사합니다. -> 깊은 복사

3. 선언적 패턴

  • 액션 - 계산 - 데이터로 코드를 분리하여 조합하는 과정에서 함수간의 계층이 생깁니다.
  • 계층을 나누고 각 계층을 침범하지 않도록 코드를 작성하다 보면 추상화 벽이 만들어 집니다.
  • 이처럼 계층이 견고해지는 구조로 작성하다 보면 상위는 행동 중심의 선언적 패턴이 생기고, 하위에는 데이터 중심의 재사용성과 테스트하기 좋은 코드가 생겨 좋은 설계뱡향의 코드가 만들어 집니다.

과제를 하면서 새롭게 알게된 점

👯‍♀️ vitest로 커스텀 훅 테스트 코드 작성하기

1. beforeEachafterEach

  • 로컬 스트리지를 모킹하여 독립적인 환경을 보장합니다.
  • vi.fn()을 통해 getItemsetItem 메서드를 모킹합니다.
  describe('useLocalStorage', () => {
    const key = 'testKey';
    const initialValue = { name: 'Test', quantity: 1 };
  
    beforeEach(() => {
      vi.stubGlobal('localStorage', {
        getItem: vi.fn(),
        setItem: vi.fn(),
      });
    });
  
    afterEach(() => {
      vi.clearAllMocks();
    });
  });

2. renderHook을 통해 훅 테스트

  test('초기값이 올바르게 설정되어야 합니다.', () => {
    (localStorage.getItem as Mock).mockReturnValueOnce(null);
    const { result } = renderHook(() => useLocalStorage(key, initialValue));

    expect(result.current.storedItem).toEqual(initialValue);
    expect(localStorage.getItem).toHaveBeenCalledWith(key);
  });

3. act를 통해 상태 업데이트 테스트

  test('setCartItem 호출 시 상태와 로컬 스토리지가 업데이트 되어야 합니다.', () => {
    const { result } = renderHook(() => useLocalStorage(key, initialValue));

    const newValue = { name: 'Updated', quantity: 3 };
    act(() => {
      result.current.setCartItem(newValue);
    });

    expect(result.current.storedItem).toEqual(newValue);
    expect(localStorage.setItem).toHaveBeenCalledWith(key, JSON.stringify(newValue));
  });

과제를 진행하면서 아직 애매하게 잘 모르겠다 하는 점, 혹은 뭔가 잘 안되서 아쉬운 것들

❓ 문제 상황

  • useLocalStorage 커스텀 훅을 만들어 장바구니 로직에 적용하니, 기본과제 테스트 코드에서 오류가 발생
  • 테스트 코드에 act 과정에서 여러 상태 업데이트를 한번에 처리하여 업데이트 된 상태가 즉시 반영되지 않음
  test('제품 수량을 업데이트해야 합니다', () => {
    const { result } = renderHook(() => useCart());

    act(() => {
      result.current.addToCart(testProduct);
      result.current.updateQuantity(testProduct.id, 5);
    });

    expect(result.current.cart[0].quantity).toBe(5);
  });


  test('합계를 정확하게 계산해야 합니다', () => {
    const { result } = renderHook(() => useCart());

    act(() => {
      result.current.addToCart(testProduct);
      result.current.updateQuantity(testProduct.id, 2);
      result.current.applyCoupon(testCoupon);
    });

    const total = result.current.calculateTotal();
    expect(total.totalBeforeDiscount).toBe(200);
    expect(total.totalAfterDiscount).toBe(180);
    expect(total.totalDiscount).toBe(20);
  });

❗️ 문제 해결 시도 방안

  • act를 각 상태 업데이트로 분리하여 상태 변경을 순차적으로 이룰 수 있도록 테스트 코드 변경
  test('제품 수량을 업데이트해야 합니다', () => {
    const { result } = renderHook(() => useCart());

    act(() => {
      result.current.addToCart(testProduct);
    });

    act(() => {
      result.current.updateQuantity(testProduct.id, 5);
    });
    
    
   test('합계를 정확하게 계산해야 합니다', () => {
    const { result } = renderHook(() => useCart());

    act(() => {
      result.current.addToCart(testProduct);
    });

    act(() => {
      result.current.updateQuantity(testProduct.id, 2);
    });

    act(() => {
      result.current.applyCoupon(testCoupon);
    });

    const total = result.current.calculateTotal();
    expect(total.totalBeforeDiscount).toBe(200);
    expect(total.totalAfterDiscount).toBe(180);
    expect(total.totalDiscount).toBe(20);
  });

🤔 문제 해결 방안에 대한 의문점

  • 현재 useLocalStoragesetItem에서 로컬 스토리지를 초기화 처리를 했는데 왜 오류가 발생할까?
  const setCartItem = (product: T | ((prev: T) => T)) => {
    try {
      const itemToStore = product instanceof Function ? product(storedItem) : product;
      setStoredItem(itemToStore);
      window.localStorage.setItem(key, JSON.stringify(itemToStore));
    } catch (error) {
      console.log(error);
    }
  };
  • 테스트 코드를 수정하지 않고 로컬 스토리지의 상태 변화를 업데이트하는 방법이 없을까?

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

폴더구조 관련

  • 어떤 프로젝트를 시작할 때 폴더구조 관련해서 많이 고민하는 편입니다.
  • 코치님께서는 코드 컨벤션처럼 폴더구조를 미리 정해두고 그 가이드에 맞춰 프로젝트를 만드시나요?
  • 아니면 프로젝트 성격에 맞게 폴더구조를 매일 다르게 하시나요?

Comment on lines +97 to +113
// 장바구니 내 모든 상품 총액 계산 함수
export const calculateCartTotal = (cart: CartItem[], selectedCoupon: Coupon | null) => {
const { totalBeforeDiscount, totalAfterDiscount: initialTotalAfterDiscount } =
calculateTotalDiscount(cart);

const { totalAfterDiscount, totalDiscount } = applyCouponDiscount(
initialTotalAfterDiscount,
totalBeforeDiscount,
selectedCoupon
);

return {
totalBeforeDiscount,
totalAfterDiscount,
totalDiscount: Math.round(totalDiscount),
};
};

Choose a reason for hiding this comment

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

함수형으로 깔끔하게 작성이 되었네요 👍

totalAfterDiscount을 initialTotalAfterDiscount으로 별칭을 만들어 준건 어떠이유에서 인지 알 수 있을까요?!
applyCouponDiscount에서 return하는 값이 totalAfterDiscount라서 그런거라면
calculateTotalDiscount를 호출하면서 가져온 리턴 값을 구조분해 하지않고 변수를 그대로 applyCouponDiscount으로 전달하면 해소가 되지 않을까?
생각이 들었습니다 !

calculateTotalDiscount에서 리턴하는 totalBeforeDiscount,totalAfterDiscount 두개의 값 모두 결국 applyCouponDiscount에 전달되기도 하니까요 !

수지님 생각은 어떠신지 궁금합니다 :)

Copy link

@hdlee0619 hdlee0619 left a comment

Choose a reason for hiding this comment

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

이번 한 주도 고생하셨습니다!

Comment on lines 498 to 508
act(() => {
result.current.addToCart(testProduct);
});

act(() => {
result.current.updateQuantity(testProduct.id, 2);
});

act(() => {
result.current.applyCoupon(testCoupon);
});

Choose a reason for hiding this comment

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

저랑 창엽님도 localStorage 훅을 적용하고 나서 테스트를 통과 못하길래 저렇게 나눠주었습니다..!
setState()의 비동기와 관련되어있는 것 같은데 정확한 원인은 모르겠습니다..ㅎㅎ README에도 의문점으로 남겨주셨더라구요..!
의문점은 해결 되셨을까요? 궁금합니다ㅎㅎ

Comment on lines +9 to +18
export const useLocalStorage = <T>(key: string, initialValue: T): Storage<T> => {
const [storedItem, setStoredItem] = useState<T>(() => {
try {
const product = window.localStorage.getItem(key);
return product ? JSON.parse(product) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});

Choose a reason for hiding this comment

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

에러처리도 하셨네요..! 꼼꼼하신 것 같습니다!

Choose a reason for hiding this comment

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

👍👍👍

Choose a reason for hiding this comment

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

SSR 환경에 대한 고려도 하신 것 같습니다! 멋져요! 💯 👍 🥇

const key = 'testKey';
const initialValue = { name: 'Test', quantity: 1 };

beforeEach(() => {

Choose a reason for hiding this comment

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

localStorage 훅을 테스트하기 위해서 이렇게 할수도 있군요 배워갑니다~


const setCartItem = (product: T | ((prev: T) => T)) => {
try {
const itemToStore = product instanceof Function ? product(storedItem) : product;

Choose a reason for hiding this comment

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

typeof product === 'function' 이런 식으로 비교하는 코드를 많이 본 것 같은데, 이 방식은 처음 봐요! 혹시 어떤 이유에서 이렇게 사용하셨는지 알 수 있을까요~? 🤓

Comment on lines +9 to +18
export const useLocalStorage = <T>(key: string, initialValue: T): Storage<T> => {
const [storedItem, setStoredItem] = useState<T>(() => {
try {
const product = window.localStorage.getItem(key);
return product ? JSON.parse(product) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});

Choose a reason for hiding this comment

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

SSR 환경에 대한 고려도 하신 것 같습니다! 멋져요! 💯 👍 🥇

if (exisitingItem) {
return cart.map((item) =>
item.product.id === product.id
? { ...item, quantity: Math.min(item.quantity + 1, product.stock) }

Choose a reason for hiding this comment

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

product.stock 들은 getRemainingStock 함수로 계산해 사용할 수 있을 것 같아요! 🤓

Copy link

@wonyoung2257 wonyoung2257 left a comment

Choose a reason for hiding this comment

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

전 테스트 코드를 작성하지 못했는데 수지님꺼 보면서 많이 배우고 갑니다 ~!

Comment on lines +57 to +59
let totalBeforeDiscount = cart.reduce((sum, item) => sum + item.product.price * item.quantity, 0);

let totalAfterDiscount = cart.reduce((sum, item) => {

Choose a reason for hiding this comment

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

두 변수 다 const로 수정할 수 있을 것 같습니다~!

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.

7 participants