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

원정·2025년 1월 17일
0
post-thumbnail

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을 순서와 상관없이 내키는 대로 해보자'라는 넉낌으로?!

💵 혹시 이 글을 보고 계신 분들께 질문

  1. ContextAPI를 다루는 각자만의 방법들이 궁금합니다.
  2. 현업에서는 사용하는 패키지도 많을텐데 패키지 버전 관리를 어떻게 하시나요?
  3. FSD를 적용하다보면 features 폴더에서 점점 컴포넌트가 증식되는 게 맞나요?
  4. Product라는 데이터를 다뤄도 다루는 위치에 따라 CartItem, Stock, Product 등으로 도메인 폴더를 나누게 됐는데, 보통 이렇게 사용하나요?

2개의 댓글

comment-user-thumbnail
2025년 1월 17일

꾸준함이 멋있네요~~ 언제 원정님이랑 뭐든 하나 해보고 싶어요!

1개의 답글

관련 채용 정보