yunwoo-yu 님의 상세페이지[2팀 유윤우] Chapter 2-3. 관심사 분리와 폴더구조

과제 체크포인트

배포 링크

https://yunwoo-yu.github.io/front_6th_chapter2-3

기본과제

목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기

  • 전역상태관리를 사용해서 상태를 분리하고 관리하는 방법에 대한 이해
  • Context API, Jotai, Zustand 등 상태관리 라이브러리 사용하기
  • FSD(Feature-Sliced Design)에 대한 이해
  • FSD를 통한 관심사의 분리에 대한 이해
  • 단일책임과 역할이란 무엇인가?
  • 관심사를 하나만 가지고 있는가?
  • 어디에 무엇을 넣어야 하는가?

체크포인트

  • 전역상태관리를 사용해서 상태를 분리하고 관리했나요?
  • Props Drilling을 최소화했나요?
  • shared 공통 컴포넌트를 분리했나요?
  • shared 공통 로직을 분리했나요?
  • entities를 중심으로 type을 정의하고 model을 분리했나요?
  • entities를 중심으로 ui를 분리했나요?
  • entities를 중심으로 api를 분리했나요?
  • feature를 중심으로 사용자행동(이벤트 처리)를 분리했나요?
  • feature를 중심으로 ui를 분리했나요?
  • feature를 중심으로 api를 분리했나요?
  • widget을 중심으로 데이터를 재사용가능한 형태로 분리했나요?

심화과제

목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기

  • TanstackQuery의 사용법에 대한 이해
  • TanstackQuery를 이용한 비동기 코드 작성에 대한 이해
  • 비동기 코드를 선언적인 함수형 프로그래밍으로 작성하는 방법에 대한 이해

체크포인트

  • 모든 API 호출이 TanStack Query의 useQuery와 useMutation으로 대체되었는가?
  • 쿼리 키가 적절히 설정되었는가?
  • fetch와 useState가 아닌 선언적인 함수형 프로그래밍이 적절히 적용되었는가?
  • 캐싱과 리프레시 전략이 올바르게 구현되었는가?
  • 낙관적인 업데이트가 적용되었는가?
  • 에러 핸들링이 적절히 구현되었는가?
  • 서버 상태와 클라이언트 상태가 명확히 분리되었는가?
  • 코드가 간결하고 유지보수가 용이한 구조로 작성되었는가?
  • TanStack Query의 Devtools가 정상적으로 작동하는가?

최종과제

  • 폴더구조와 나의 멘탈모데일이 일치하나요?
  • 다른 사람이 봐도 이해하기 쉬운 구조인가요?

과제 셀프회고

⚠️ 심화과제까지의 FSD구조는 fsd_old 폴더에, 나만의 구조 형태는 src 폴더에 있습니다.

FSD가 나름 명확한 기준이 있는 아키텍처라고 생각해서 쉽게 분리할 수 있을 줄 알았는데 또 생각보다 모호하고 익숙치 않은 부분들이 있어 클린코드 챕터보다 오래 걸린 것 같습니다^^..

조금 아쉬운건 API가 완벽하지 않아 기능은 동작하지만 정상적이지 못한 부분들이 있는게 조금 아쉽습니다. (검색에서는 오름차순 내림차순이 안된다던가 하는 부분)

또 Suspense를 사용하여 fallback을 선언적으로 관리하려 했지만 그러려면 useSuspenseQuery를 사용해야하는데 현재 구조에서는 게시물 리스트 API, 검색 게시물 리스트 API, 태그 게시물 리스트 API가 존재하여 처음에 API를 무조건 3개를 다 불러와야한다는 부분(useSuspenseQuery는 enabled 옵션이 존재하지 않고 반드시 유효한 값을 반환해야함)이 너무 별로인 것 같아 if(isLoading) 을 통해 명령형 방식을 채택한 부분이 조금 아쉽습니다.

이번 과제는 FSD를 처음 접하면서 “구조를 통해 복잡성을 줄인다”는 감각을 몸으로 익힌 시간이었습니다. app/pages/widgets/features/entities/shared가 각각 어떤 책임을 져야 하는지, 그리고 index.ts를 통해 공개 API를 관리하는 방식을 직접 적용해보며 확실히 이전 코드보다는 나아짐을 느꼈지만 구성 자체가 복잡해져 모르는 사람이 봤을 땐 더 어려울 수 있겠다는 생각도 들었습니다.

각 레이어의 기준

app

  • layout : 이름 그대로 페이지의 Layout 컴포넌트를 위치했습니다. 적용은 Routes 구성 시 적용합니다.
  • providers : 모든 글로벌한 providers를 묶어 적용합니다. 현재는 TanStack Query Provider만 존재합니다.
  • routers : BrowserRouter가 존재합니다.
  • styles : 전역 스타일인 globals.css가 존재합니다. 추가적으로 theme, color 같은 파일도 존재할 수 있습니다.
  • types : 전역 타입 모듈 선언 파일이 존재합니다.
  • App.tsx : Provider, Router가 적용된 App 파일입니다.

pages

  • ui : 각 페이지 파일이 존재합니다. 네이밍으로 페이지를 명확하게 구분합니다.

widgets

  • footer, header, postTable ... : 각 위젯의 역할이 담긴 폴더로 구분하며 내부에는 segment 역할의 폴더들로 나누었습니다.

features

  • post, comment : slice 역할인 비즈니스 영역 폴더
    • ui, model/hooks : 각 기능 역할을 하는 AddPostButton, AddPostDialog 등이 존재하며 서버 요청을 하는 hooks 폴더가 존재합니다.

entities

  • slice 영역 : post, comment, tag, user가 존재합니다. 기준은 서버에서 GET 해오는 비즈니스 데이터가 있냐 없냐 여부로 나누었습니다.
  • model : type과 queryKeys 가 존재하며 서버 상태를 가져오는 훅들을 모은 hooks, UI 상태를 관리하는 stores로 내부적으로 폴더를 구분했습니다.

shared

  • api : api 요청을 보내는 http 유틸, 쿼리 키 팩토리 함수 등 api 요청에 필요한 함수들을 담았습니다.
  • lib : 각종 공유되는 함수들을 모아두는 폴더입니다.
  • ui : 아토믹한 단위의 재사용 컴포넌트들을 모아두는 폴더 입니다.

entities, features 규칙 정하기

두 폴더 모두 비즈니스 로직이 엮여있어 어떤게 entities, features 인지 명확히 머릿속에서 구분이 되지 않았습니다. 그래서 저만의 규칙을 세워 두 영역을 구분했습니다.

  • entities : 비즈니스 도메인이 엮인 GET 요청에 대한 hooks, types 및 액션 함수들
  • features : 비즈니스 도메인이 엮인 GET 을 제외한 액션 hooks 및 UI

추상적인 규칙보다는 코드 레벨에서 명확한 기준을 두어 나눌 수 있게 해봤습니다.

index.ts

slice가 존재하는 레어에 대해서는 slice 레벨에서 index.ts 파일을 생성하여 진입점을 구분하였습니다. slice가 없는 app, shared 같은 경우는 각 segment 폴더 내부에서 진입점을 만들어 추가하였습니다.

더 고민이 필요한 부분

shared, app 같은 경우 slice가 존재하지 않아 일관성이 좀 떨어지는 것처럼 느껴졌습니다. 모든 레이어가 똑같은 구조를 가지면 좋을 것 같은데 조금 더 고민이 필요할 것 같습니다.

앞으로 개발에 적용할 부분

app, features, entities, shared 레이어가 굉장히 좋다고 생각이 들었습니다. 단 내부 slice와 segment에 대해 조금 조정하여 적용해볼 예정입니다.

최종 FSD "맛" 첨가한 구조

FSD의 구조만 참고하고 조금 더 React 친화적인 구조로 만들어봤습니다.

src ㄴ app - 각종 글로벌한 설정이 존재  ㄴ providers  ㄴ routers  ㄴ types  ㄴ App.tsx ㄴ pages - 페이지이름의 폴더가 존재하며 각 페이지에서만 사용하는 컴포넌트 or hook or utils 가 존재한다.  ㄴ AuthPage   ㄴ components - widgets 역할 컴포넌트 끼리의 layout 및 Section 역할의 컴포넌트들   ㄴ hooks - Page 레벨에서의 재사용 훅   ㄴ utils - Page 레벨에서의 재사용 함수   ㄴ AuthPage.tsx ㄴ entities - 비즈니스가 엮여있으며 해당 비즈니스 도메인 내에서 재사용이 되는 부분들 (Auth, Register, Login 등)  ㄴ auth   ㄴ model - 비즈니스 데이터와 연관된 순수함수   ㄴ hooks    ㄴ queries - 앤티티가 엮인 query 훅    ㄴ mutations 엔티티가 엮인 mutations 훅   ㄴ const.ts (Options)   ㄴ types.ts (Options) ㄴ features - 비즈니스가 엮여있으며 해당 비즈니스 레벨에서만 사용하는 액션이 있는 컴포넌트  ㄴ auth   ㄴ AuthForm   ㄴ KakaoLogin ㄴ shared - 2개 이상의 비즈니스 레벨에서 재사용 가능한 코드들 (하지만 코드는 순수한 상태)  ㄴ components - Button, Input 등 아토믹한 단위의 UI 컴포넌트  ㄴ hooks - useDebounce, useForm 등 재사용률이 높은 훅  ㄴ lib   ㄴ utils - fomatter, validator 등 순수함수   ㄴ types - 제네릭한 타입들 공통 타입들 ApiResponse, FormField 등   ㄴ api - axios instance, intercepter 등   ㄴ constants - 공용 상수 데이터 관리

물론 많은 시행 착오가 필요하겠지만 React에 친화적이면서 FSD의 장점인 레이어 구조도 사용하고 역할이 명확히 나누어져있다고 생각되는 구조로 만들어보았습니다. entities에 const와 types가 Options인 이유는 응집도를 높이기 위해 각 훅 파일과 컴포넌트 레벨에서 타입을 지정하는게 좋다고 생각하기 때문입니다.

챕터 셀프회고

클린코드와 아키테쳑 챕터 함께 하느라 고생 많으셨습니다! 지난 3주간의 여정을 돌이켜 볼 수 있도록 준비해보았습니다. 아래에 적힌 질문들은 추억(?)을 회상할 수 있도록 도와주려고 만든 질문이며, 꼭 질문에 대한 대답이 아니어도 좋으니 내가 느꼈던 인사이트들을 자유롭게 적어주세요.

클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기

  • 더티코드를 접했을 때 어떤 기분이었나요? ^^; 클린코드의 중요성, 읽기 좋은 코드란 무엇인지, 유지보수하기 쉬운 코드란 무엇인지에 대한 생각을 공유해주세요

놀라우면서도 친숙한 코드였습니다 ^^.. 가끔 외주사에서 온 코드가 이 정도는 아니였지만 많이 더러운 상태였던 경우가 있습니다. 클린코드는 같이 일하는 사람과의 매너라고 생각합니다. 읽기 좋은 코드, 유지보수하기 쉬운 코드는 한눈에 들어올만큼 응집도는 높고 결합도는 낮은 그런 코드라고 생각합니다. 하지만 가장 중요한 건 동료들과 함께 정한 컨벤션, 규칙을 따른 코드라고 생각하며 이렇게 작성된 코드는 함께하는 동료들에게는 감흥없지만 그냥 납득이 되는 코드기에 이론적인 클린코드, 유지보수성이 높은 코드 전에 가장 중요하게 지켜야 할 포인트라고 생각했습니다.

결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리

  • 거대한 단일 컴포넌트를 봤을때의 느낌! 처음엔 막막했던 상태관리, 디자인 패턴이라는 말이 어렵게만 느껴졌던 시절, 순수함수로 분리하면서 "아하!"했던 순간, 컴포넌트가 독립적이 되어가는 과정에서의 깨달음을 들려주세요

가장 크게 와닿았던 부분은 컴포넌트 에서는 각 액션을 처리하기 위한 handler 함수만 남았을 때 인 것 같습니다. 각 기능의 역할은 훅으로 빼서 추상화하며 컴포넌트에서는 각 함수의 역할을 네이밍을 보고 명확히 알 수 있는 형태가 이상적이라고 느껴졌던 것 같습니다. 순수함수의 분리는 아직 감이 잘 오지 않기에 조금 공부해야할 것 같습니다.

응집도 높이기: 서버상태관리, 폴더 구조

  • "이 코드는 대체 어디에 둬야 하지?"라고 고민했던 시간, FSD를 적용해보면서의 느낌, 나만의 구조를 만들어가는 과정, TanStack Query로 서버 상태를 분리하면서 느낀 해방감(?)등을 공유해주세요

TanStack Query를 적용하면서 가장 좋은건 서버 상태를 받아 useEffect를 통해 useState에 데이터를 집어넣을 필요가 없다는 부분이 가장 좋은 것 같습니다. 이를 통해 useState 및 전역상태들은 View만을 위한 상태들로 관리가 가능해져 역할에 명확함이 생긴게 너무 큰 장점인 것 같습니다.

FSD 적용 전 기존에 회사에서 사용하던 구조는 점점 프로젝트의 규모가 커져갈수록 비대해지는 components, hooks가 큰 문제였습니다. FSD를 적용한다면 이런 문제가 바로 해결되기에 다들 비슷한 고민들을 가지고 있었고 이 고민들을 나름의 해결방법으로 나온게 FSD구나 하지만 프로젝트 규모에 맞추어 사용하는게 가장 중요하다는걸 또 깨닫게 되었습니다. 현재 과제같은 페이지 하나짜리의 프로젝트라면 FSD는 확실히 오버엔지니어링이니까요.

모든걸 경험하면서 느낀건 개방 폐쇄 원칙 - OCP (Open Closed Principle)이 떠올랐습니다. 디자인패턴이든, FSD든, 클린코드든 모두 확장에는 열려있으며 수정에는 닫히게 만든다는 느낌이 강하게 들었습니다. 아마 많은 선배 개발자분들이 React가 나오기 이전부터 겪었던 문제들에 대한 솔루션과 원칙들이 현대 개발에도 도움이 되기에 진짜진찌로 책을 많이 읽어 개념적인 부분들을 충분히 익혀두는게 좋겠다 생각했습니다.

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

  1. 제가 만든 최종 형태의 폴더 구조의 단점이나 빠진 고려해야할 부분(또는 추가되면 좋을)이 있을까요!?

  2. entities쪽에 tag, user에 대한 GET API가 존재하여 별도 slice 폴더로 구분하였습니다. 근데 또 막상 tag 리스트는 게시물 리스트 필터에 이용되고, user또한 게시물쪽 데이터 추가에만 사용되는 구조인데 나누는게 좋았을까요? 지금 보니 post로 편입하는게 나았을까? 하는 생각이 듭니다.

  3. 현재 과제에서 분리하면 좋겠다! 싶은 순수함수를 찾지 못했습니다. 코치님이 보시기에 분리하면 좋을법한 부분이 있을까요?

과제 피드백

안녕하세요 윤우님! 6주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!

제가 만든 최종 형태의 폴더 구조의 단점이나 빠진 고려해야할 부분(또는 추가되면 좋을)이 있을까요!?

안녕하세요 윤우님! 폴더 구조는 무척 잘 만들어주셨다고 생각해요! 현재 미션에서 추가적으로 더 고려할점은 딱히 보이질 않네요..! 개인적으로 FSD에 대해 더 디테일하게 고민해보기 위해선 여러 개의 페이지가 필요하다고 생각해요. 다만 지금은 페이지가 한 개라서 아쉽네요 ㅎㅎ

entities쪽에 tag, user에 대한 GET API가 존재하여 별도 slice 폴더로 구분하였습니다. 근데 또 막상 tag 리스트는 게시물 리스트 필터에 이용되고, user또한 게시물쪽 데이터 추가에만 사용되는 구조인데 나누는게 좋았을까요? 지금 보니 post로 편입하는게 나았을까? 하는 생각이 듭니다.

흠.. 지금 보니 src 폴더가 있고 fsd_old 폴더가 있는데, old에서의 상황을 말씀해주시는 것 같군요 ㅎㅎ 결국 각 엔티티간의 연관성 때문에 합칠까 말까를 고민하는 상황처럼 보여요. 저는 이럴 때는 분리하는게 좋다고 생각합니다..! 딱 지금 미션만 생각하면 분리할필요 없겠지만, 실무에서의 상황을 생각해보면 분리해놓는게 추후를 위해 좋지 않을까!? 라는 생각입니다. 어디서 어떻게 쓰일지 예측이 안 되니까요!

현재 과제에서 분리하면 좋겠다! 싶은 순수함수를 찾지 못했습니다. 코치님이 보시기에 분리하면 좋을법한 부분이 있을까요?

if (variables.postId && currentUser) {
  queryClient.setQueryData(COMMENT_QUERY_KEY.detail([variables.postId]), (oldData: GetCommentResponse) => {
    if (!oldData) return oldData

    // 새 댓글을 목록에 추가 (userId 1의 실제 사용자 정보 사용)
    return {
      ...oldData,
      comments: [
        ...oldData.comments,
        {
          id: Date.now(), // 임시 ID
          body: variables.body,
          postId: variables.postId,
          user: {
            id: currentUser.id,
            username: currentUser.username,
            image: currentUser.image,
          },
          likes: 0,
        },
      ],
      total: oldData.total + 1,
    }
  })
}

이게 useCrateComment 내부의 함수인데요, 여기서 새로운 comment를 만들어내는 부분도 순수함수로 분리할 수 있다고 생각해요. 이렇게 쓰이는 모습을 상상하시면 좋답니다!

if (variables.postId && currentUser) {
  queryClient.setQueryData(COMMENT_QUERY_KEY.detail([variables.postId]), (oldData: GetCommentResponse) => {
    if (!oldData) return oldData

    // 새 댓글을 목록에 추가 (userId 1의 실제 사용자 정보 사용)
    return {
      ...oldData,
      comments: [...oldData.comments, createNewComment(variables, currentUser)],
      total: oldData.total + 1,
    }
  })
}

이 외에도 훅 내부에서 데이터를 다루는 구간은 다 분리할 수 있으리라 생각해요 ㅎㅎ

이렇게 컴포넌트 내부에 있는 로직도 마찬가자입니다.

  const [searchParams, setSearchParams] = useSearchParams()

  const handleSortOrderChange = (sortOrder: string) => {
    setSearchParams((prev) => {
      const updated = new URLSearchParams(prev)

      updated.set("sortOrder", sortOrder)
      return updated
    })
  }

이런 모습을 상상해보시면 좋을 것 같아요.

  const [searchParams, setSearchParams] = useSearchParams()

  const handleSortOrderChange = (sortOrder: string) => {
    setSearchParams((prev) => updateQueryParams({ sortOrder }))
  }

아니면 별도의 훅을 하나 만들어서 사용한다거나!?