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

[11팀 장원정] [Chapter 2-2] 디자인 패턴과 함수형 프로그래밍 #41

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

Conversation

wonjung-jang
Copy link

@wonjung-jang wonjung-jang commented Jan 16, 2025

과제 체크포인트

기본과제

  • React의 hook 이해하기

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

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

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

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

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

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

심화과제

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

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

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

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

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

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

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

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

[항해 플러스 프론트엔드 4기] 5주차 과제 회고

5주차 과제는 <Chapter 2-2. 디자인 패턴과 함수형 프로그래밍>이다.
이번 과제에도 두 개의 페이지 컴포넌트에 모든 로직이 몰려있는 코드가 주어졌다.
이를 분리하고 클린하게 만드는 것이 목표다.

과제를 진행하며 개인적인 목표는

  1. 액션, 계산, 데이터 분리 및 액션은 줄이고 계산을 늘리기.
  2. FSD 적용.

이다.

💰 함수형 프로그래밍 적용해보기


이번 과제에서 함수형 프로그래밍을 고려하며 진행했냐 묻는다면, 아니다.

  • 컴포넌트를 나누다가, props drilling이 생기네? -> ContextAPI로 분리.
  • 컴포넌트에 상태와 상태를 활용한 함수가 많네? -> 훅 분리

거의 위 두 가지 기준만 사용하지 않았을까 싶다.

처음에는 shared

export const push = (arr, item) => [...arr, item];

이런 추상화 함수들을 작성하다가, '굳이?'라는 의문이 들어서 삭제했다.

const handleEditProduct = (product: Product) => {
    setEditingProduct({...product});
  };

  const handleProductNameUpdate = (productId: string, newName: string) => {
    if (editingProduct && editingProduct.id === productId) {
      const updatedProduct = { ...editingProduct, name: newName };
      setEditingProduct(updatedProduct);
    }
  };

  const handlePriceUpdate = (productId: string, newPrice: number) => {
    if (editingProduct && editingProduct.id === productId) {
      const updatedProduct = { ...editingProduct, price: newPrice };
      setEditingProduct(updatedProduct);
    }
  };

  const handleEditComplete = () => {
    if (editingProduct) {
      onProductUpdate(editingProduct);
      setEditingProduct(null);
    }
  };

  const handleStockUpdate = (productId: string, newStock: number) => {
    const updatedProduct = products.find(p => p.id === productId);
    if (updatedProduct) {
      const newProduct = { ...updatedProduct, stock: newStock };
      onProductUpdate(newProduct);
      setEditingProduct(newProduct);
    }
  };

그나마 추상화했다면 위 함수들을

const handleProductUpdate = (key: string, newValue: string | number) =>
    setEditingProduct({ ...editingProduct, [key]: newValue });

이렇게 하나의 함수로 합쳤다.
handleEidtProduct는 어차피 setEditingProduct와 다를 바 없어서 삭제하고, 나머지는 value만 받는 것이 아닌, key도 함께 받아서 처리하도록 했다.

비단 함수뿐만 아니라 컴포넌트도 그렇다.

export function Input({ id, type, value, onChange }: InputProps): JSX.Element {
  return (
    <input
      id={id}
      type={type}
      value={value}
      onChange={onChange}
      className="w-full p-2 border rounded"
    />
  );
}

위와 같은 컴포넌트를 만들었다.
className, 즉 스타일만 공통으로 사용하고 나머지는 다 외부에서 받는데 input 태그를 그냥 쓰는 거랑 크게 다르지 않은 것 같다.

이런 고민이 생겨서 멘토링 시간에 코치님께 질문했다.

코치님께서는 스타일만 미리 정의한다는 건 같지만, 훨씬 더 유연한? 컴포넌트로 만들어서 설명해주셨다.

디자인 시스템 오픈 소스를 참고해보며 어떻게 공통으로 사용할 컴포넌트를 유연하게 정의할지 고민해봐야겠다.

추가로 TypeScript를 잘 써야겠다고 생각했다.

💰 FSD 적용해보기


코치님의 블로그에서 FSD가 탄생한 과정이 자세히 나와있습니다.
FSD에 대해서 자세히 알고 싶다면 https://velog.io/@teo/separation-of-concerns-of-frontend 이 글을 참고해주세요!

패드에 위 내용을 적어놓고 파일을 생성할 때마다 어디에 넣어야할지 생각했다.
처음이라서 그런지 명확하게 구분하기 어려웠다.

가령 widgets은 '재사용이 가능한'이라는 단어에 중점을 둬서 아무것도 넣지 못했다가, features의 볼륨이 커져서 페이지 컴포넌트를 구성하는 섹션을 넣는 것으로 중간에 바꿨다.

그럼에도 컴포넌트를 계속 분리하면서 features의 볼륨이 커졌는데, 그렇다고 entities나 shared로 내리는 건 아닌 것 같아 계속 features에서 컴포넌트를 분리했다.

전역 관련한 내용도 헷갈렸다.
전역이란 말에 app에 넣었다가, 전역적으로 사용하는 거라 shared로 옮겨 넣는 경우도 있었다.
전역 공간에서 전역적인 설정을 하는? 파일을 app에, 어디서든 사용할 수 있는 것은 shared에 넣는 걸로 나름의 재정의를 하기도 했다.

컴포넌트는 그렇다고 해도 훅이나, 유틸 함수들, 국소적으로 적용할 Provider는 어느 레이어에 둬야할지 알지 못했다.

과제를 하며 대부분의 시간을 폴더를 고민하는 데에 쓰는 걸 자각하고 중간에서 부터는 크게 고민하지 않고 사용하는 컴포넌트랑 같은 레이어에 두는 걸로 하고 고민하는 시간을 줄였다.

두 개의 페이지 컴포넌트에 모든 로직이 몰려있어서, 분리하면서 적용해봐야겠다고 생각했는데, 그 전에 정리하는 과정을 거쳤으면 더 수월하지 않았을까 싶었다.

💰 ContextAPI


다른 분들은 ContextAPI를 사용할 때 어떤 방법으로 사용하시나요?

과제를 하다가 ContextAPI에 관련해서 궁금한 점이 생겨서 화면 공유를 했다.

export function ProductContextProvider({
  initialProducts,
  children,
}: {
  initialProducts: IProduct[];
  children: React.ReactNode;
}) {
  return (
    <ProductContext.Provider value={useProducts(initialProducts)}>
      {children}
    </ProductContext.Provider>
  );
}

위 코드를 보여주자마자 질문은 뒷전이고, 팀원분들께서 경악을 금치 못했다.
Provider에서 직접 훅을 호출했기 때문이다.

useProducts의 결과값을 변수에 담고 넘겨주는 과정이 불필요하지 않을까 생각해서 했던 행동이었다.

공유를 하고 팀원분들의 반응을 보기 전까지 잘 못된 건지도 몰랐다.

이래서 여러 사람의 코드를 보고 내 코드도 보여주면서 피드백을 받아야 하나보다.

아무튼 이를 수정해서 아래와 같은 코드로 바꿨다.

export function ProductContextProvider({
  initialProducts,
  children,
}: {
  initialProducts: IProduct[];
  children: React.ReactNode;
}) {
  const productContextValue = useProducts(initialProducts);

  return (
    <ProductContext.Provider value={productContextValue}>
      {children}
    </ProductContext.Provider>
  );
}

재밌게도 이 사용법에서도 여러 의견이 나왔다.
개인적으로 저기서 뭔가를 더 한다면 useProducts의 내용을 Provider 내부에 선언해서 데이터를 다루는 Context와 업데이트 함수들을 다루는 Context를 따로 생성하여 하나의 Provider로 내보내는 방법을 사용한다.

팀원분 가운데 한 분은 Provider 내부에서 useState를 사용해서 상태만 선언한 뒤 훅에서 useContext를 통해 데이터를 받고, 업데이트 함수들을 훅 안에 작성하여 훅을 통해 상태와 함수를 받을 수 있도록 사용한다고 하셨다.

다른 팀원분은 나와 비슷하지만 Provider 내부에서 훅을 호출하진 않을 것 같다고 하셨다.

한 분은 보편적인 방법이라고 설명했다가, 다른 분의 의견이 다른 걸 듣고 자기의 경험 안에 갇혀서 생각하신 것 같다는 말씀을 하셨다.

한 분은 다른 분들은 어떻게 사용하는지 모른다고 하셨다.

나는 주변에 프론트엔드 개발자가 항해분들 밖에 없다.

보편적인 방법을 사용하고 싶은데, 어떤 방법이 보편적인 걸까?

💰 전역 상태 관리 라이브러리의 필요성


ContextAPI를 사용하면서 전역 상태 관리 라이브러리의 필요성을 느꼈다.
전역 상태 관리 라이브러리를 제대로 사용해본 경험이 없어서 얼마나 편할지는 모르지만, ContextAPI를 사용하며 겪은 귀찮음?들이 대부분 해소가 된다고 해서 다음부터는 써봐야겠다.

앞서 말한 것처럼, 데이터와 업데이트 함수를 나누면 데이터만 사용하는 컴포넌트는 데이터가 업데이트 될때만 리렌더링되고, 업데이트 함수만 사용하는 컴포넌트는 데이터가 바뀌어도 리렌더링되지 않는다.

하지만 Context를 만들 때마다 두개씩 만들어야 하는 번거로움이 있다.
분리한다고 해도 데이터와 업데이트 함수를 같이 사용하는 경우가 많다면 굳이 분리할 필요도 없다.

'최적화를 해줘야 하지 않을까?'라는 생각에서 오는 불편함도 한 몫했다.
보통 Provider 내부에서 업데이트 함수들을 useCallback으로 감싸고 valueuseMemo로 감싼다.

하지만 Provider에서 사용하는 상태가 하나면 굳이 해야 할까?
어차피 리렌더링 되어야 하는 거 아닐까?
만약 두 개면 분리할 수는 없을까?
어쩔 수 없이 두 개를 써야한다면 useCallback의 의존성 배열에 뭘 넣어야할지 고민해야 한다.

전역 상태 관리 라이브러리는 알아서 최적화를 해주고, 전역 상태를 훅에서 호출해서 업데이트 함수와 함께 내보내기만 하면 되니까 위에 모든 고민을 하지 않아도 되지 않을까?!

💰 package.json 버전 문제


과제를 제출하고 한가로이 쇼츠 지옥에 빠져있을 때, 슬랙 메세지가 왔다.

깃 레파지토리를 확인해보니 패키지 버전 충돌로 인해 npm install이 되지 않았다.

팀 컨벤션으로 사용하던 airbnb 린트 컨벤션이 있었는데, typescript 린트 플러그인과 버전이 맞지 않아서 발생한 문제였다.

급한대로 에어비앤비 린트를 삭제했는데, 실무에서는 이보다 다양한 패키지들이 설치되었을 텐데 어떻게 관리를 할까 궁금해졌다.

하나가 꼬이면 다 꼬이는 거 아닌가?

팀원 분께서 에어비앤비 린트는 워낙 예전에 나온 거기도 해서, 요즘에는 기본 reacttypeScript 플러그인만 써도 충분할 거라고 하셨다.

💰 마치며


함수형 프로그래밍을 공부했지만, 잘 적용하지 못했다.
FSD를 시도했지만, 잘 적용하지 못했다.
여러모로 아쉬움이 남는 과제였다.

💵 Keep: 현재 만족하고 계속 유지할 부분

지난 번과 마찬가지로 스터디를 하긴 잘 한 것 같다.
과제 진도가 안 나가는 날 하필 내 발표날이었다.
책을 보는데 추상적인 표현이 많아서 잘 이해되지 않았다.
만약 스터디를 하지 않았다면, 책을 덮고 과제를 했을 거다.
하지만 발표를 해야 하기 때문에 읽고 정리했다.
이해가 되지 않은 상태로 정리를 했지만, 정리하는 과정에서 어느 정도 이해가 됐다.
이래서 약속을 만들어서 책임감을 엮는게 중요하다고 생각했다.

💵 Problem: 개선이 필요하다고 생각하는 문제점

과제를 제출하고 나서 항상 하는 생각인데, 과제를 할 때 깊은 고민없이 하게 되는 것 같다.

깊은 고민으로 나온 명확한 기준을 갖고 코드를 작성해야 할 것 같고, 필요성을 느껴 다음 과제에는 꼭 해봐야겠다 생각하지만 지켜지지 않는다.

💵 Try: 문제점을 해결하기 위해 시도해야 할 것

여러 할 일들을 쪼개놓고 어느정도 타임 테이블을 정해놔야할 것 같다.
'이 시간에는 이걸 해야해!'보다는 '이 동안은 1, 2, 3을 순서와 상관없이 내키는 대로 해보자'라는 넉낌으로?!

💰 리뷰받고 싶은 내용

  • ContextAPI를 어떻게 사용하냐는 점에서 팀원 분들께서 다양한 의견이 나왔습니다.
    • Context에서는 상태만 선언하고 커스텀 훅 내부에서 useContext로 상태를 불러와서 함수를 작성하고 내보내는 방법
    • Context의 Provider에서 상태와 함수를 모두 작성하는 방법.
    • Context의 Provider에서 훅을 호출해서 사용하는 방법.
    • 등등 여러 의견이 나왔는데, 각자만의 사용법이 있겠지만 보편적인 방법은 뭘까요? 최대한 보편적인 방법으로 사용하고 싶은데 보편적이라는 기준을 국내 프론트엔드 개발자가 보통 사용하는 방법이라고 한다면, 어떤 방법이 보편적인지는 어떻게 알 수 있을까요?
    • 꼭 보편적인 방법이라고 보다 어떤 방법을 사용하시는지 각자만의 여러 의견을 듣고 싶습니다!
  • 현업에서는 사용하는 패키지도 많을텐데 패키지 버전 관리를 어떻게 하시나요?
  • FSD를 적용하다보면 features 폴더에서 점점 컴포넌트가 증식되는 게 맞나요?
  • Product라는 데이터를 다뤄도 다루는 위치에 따라 CartItem, Stock, Product 등으로 도메인 폴더를 나누게 됐는데, 보통 이렇게 사용하나요?

장원정 added 28 commits January 13, 2025 00:42
- eslint aribnb config 적용
- prettier 설정 추가
- CartPage, AdminPage와 cart관련 훅, 함수 pages 레벨로 분류
- AdminPage를 product, coupon으로 나눠 features 레벨로 분류
Comment on lines +9 to +12
const getRemainingStock = (product: IProduct) => {
const cartItem = cart.find((item) => item.product.id === product.id);
return product.stock - (cartItem?.quantity || 0);
};

Choose a reason for hiding this comment

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

해당 부분도 계산함수로 분리할 수 있을것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

아하 cart를 인자로 받으면 계산으로 뺄 수 있겠네요!

Comment on lines +13 to +15
const calculateTotal = (cart: ICartItem[]) => {
return calculateCartTotal(cart, selectedCoupon);
};

Choose a reason for hiding this comment

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

이 부분도 비슷한 생각입니다.
저는 훅 내에서는 모든 계산 함수들을 제외했는데 혹시 훅과 계산함수들에 대한 분리 기준이 따로 있으신걸까요?

Copy link
Author

Choose a reason for hiding this comment

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

아 저는 기존에 컴포넌트에 있는 로직을 일단 훅으로 빼고 거기서 계산을 따로 뺄 생각을 안 한 것 같습니다 ㅠㅠ
근백님이 남겨주신 코멘트 보니 계산으로 뺄 함수들이 많았네요!

Comment on lines +4 to +7
export const getRemainingStock = (cart: CartItem[], product: IProduct) => {
const cartItem = cart.find((item) => item.product.id === product.id);
return product.stock - (cartItem?.quantity || 0);
};

Choose a reason for hiding this comment

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

useCart 에서 봤던거 같은데 여기서는 따로 분리하셨네요?
둘 중하나는 제거하는게 좋을것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

엇 그러네요!

Comment on lines +8 to +9
const { editingProduct, handleRemoveDiscount } = useEditingProductContext();
const discount = editingProduct.discounts[index];

Choose a reason for hiding this comment

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

저도 이렇게 처리할지 그냥 product 를 넘길지 고민을 했었는데 이부분에 대해서도 서로 얘기해보면 좋을거같아요 !!

Choose a reason for hiding this comment

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

Coupon 적용부를 따로 나누셨군요 이렇게 하는편이 더욱 확장성이 있어보이네요! bb!

@Geunbaek
Copy link

원정님은 entities 를 앱내에서 사용되는 모든 데이터들의 정의 및 변경을 다루고 있다고 보면되겠죠?
features 는 말 그대로 기능을 나타내는 동사? 같은 느낌으로 보이는데 제가 이해한게 맞을까요?
위의 정의가 맞다면 위와같이 나누는 것도 좋을것 같네요 bb!

제가 분류한 기준은 뭔가 entities 와 features 의 경계가 좀 모호해서요 . 이렇게 나눈다면 조금더 명확한것 같습니다.
저도 한번더 생각한 뒤 이번 프로젝트에 적용해봐야할것 같습니다 감사합니다 !

@wonjung-jang
Copy link
Author

@Geunbaek 엇 좋게 말씀해주셨지만 사실 저도 기준이 모호합니다... 기준을 어덯게 잡아야 할지 아직 감이 잘 안 잡히네요!

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.

2 participants