j2h30728 님의 상세페이지[6팀 이지현] Chapter 2-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가 지향하고자하는 바를 반영하지 못했던 것 같아요.

저는 더티코드나 레거시코드를 리팩토링할 때에는 뷰를 먼저 분리하기보단, 데이터나 상태를 중심적으로보고 리팩토링을 하는것을 선호하는 것 같습니다. 이번 과제에서는 사용되는 타입들을 분석하고 entities/model/types 을 채우는 것으로 시작했 던 것 같습니다. 아래의 순서로 리팩토링했습니다.

  1. 사용하는 타입 확인 : 전체적인 타입 체크
  2. 타입 기반 데이터, 상태 확인 : api 호출 타입과 그것을 활용해서 상태에서 어떤 타입의 값을 사용하는 지
  3. entities/sliced/model - type 생성
  4. entities/sliced/api 생성
    • api 호출 함수 위치가 정말 많이 고민돼서 주변사람들에게도 물어봤는데, 정말 50:50 로 entities와 features에 작성하시더라구요. 저는 데이터를 조작하는 관점에서 entities에 넣었습니다. 과제 진행 이후에 클로드에게 한 번 더 물어보니, 같은 행위여도 비즈니스 로직관점에서 달라질 수 있다는 것을 깨달았습니다. 그렇게 복잡한 비즈니스 관점이라면 feature에 api 호출을 넣는 것도 좋을 것 같습니다.
  5. feature/widget -query훅 및 비즈니스 로직 커스텀 훅 생성
  6. feature/widget - 컴포넌트 분리

컴포넌트 분리는 데이터에 비해서 항상 어려운 것 같습니다. 그런데 feature/widget 에서 불러오는 도메인 함수를 비교하면서 분리 및 파일링을 하려고하니, 좀 더 편하고 쉬웠습니다. 계층간 역전되지 않게 하며 동일한 도메인으로 넣어준다는 관점으로 보니 파일링이 쏙쏙 들어갔습니다.

물론 shared에 들어가는 부분은 아직도 고민이 많지만요.

이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.

  • 결합도는 떨어뜨리고 응집도를 높이자. 5주차 과제에서 도메인중심으로 설계하였지만, 커스텀 훅 뿐만 아니라 모든 로직을 분리되지 않은 코드를 작성했습니다. 어떻게 나눠야할지 고민을 많이 했었지만 해결하지 못한채로 제출했었습니다.

이번 과제를 통해서 도메인중심을 넘어서 결합도는 떨어뜨리되 응집도는 높이는 경험을 해볼 수 있었습니다.

src/
├── app/           # 애플리케이션 진입점 및 글로벌 설정
├── entities/      # 비즈니스 엔티티 (post, comment, user, tag)
├── features/      # 사용자 기능 단위 (add-post, delete-comment 등)
├── widgets/       # 복합 UI 블록 (post-detail-modal, posts-with-users)
├── pages/         # 페이지 컴포넌트
└── shared/        # 공통 리소스 (api, ui, hooks, store)

  • entities : 순수한 데이터 조작
  • feature : 사용자 액션 처리
  • widget : 두 개 이상의 도메인 조합

본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?

1. 명확한 계층과 슬라이스

가장 노력했던 부분은 "이 코드가 어느 레이어에 속해야 하는가?"를 일관되게 판단하는 것이었습니다. 예를 들어 댓글 추가 기능을 구현할 때, 단순히 API 호출만 하는 로직은 entities/comment/api에, 실제 사용자 액션과 비즈니스 로직은 feature/add-comment에 분리하려고 노력했습니다.

  • entities/post/api/ : 순수한 Post CRUD API
  • features/add-post/ : 게시물 추가라는 사용자 액션 + UI + 비즈니스 로직
  • widgets/post-detail-modal/ : 게시물 상세보기 + 댓글 목록이라는 복합 UI 블록
  • shared/hooks/useModal.ts : 모달 상태 관리라는 공통 로직
// entities에서 순수 데이터 조작
entities/comment/api/index.ts → commentApi.createComment()

// features에서 사용자 액션과 비즈니스 로직
features/add-comment/api/queries.ts → useAddCommentMutation()

// widgets에서 복합 UI 조합
widgets/post-detail-modal/ → 게시물 상세 + 댓글 목록 + 액션 버튼들

2. colocation

평큐에서 테오와 수강생 대화를 들으며, fsd 에서 레이어 위치시키는 다양한 요소중에 하나가 코로케이션이더라구요. 응집도를 올리기위한 작업으로 이해했는데, 컴포넌트를 나누면서 해당 대화가 문득 떠올랐습니다.

컴포넌트를 분리하고 조합하고, 함께 두는 작업을 하면서 기억에 남았던 컴포넌트는 모달버튼 컴포넌트였습니다. 어느정도 진도가 진행된 저의 PostsManagerPage 컴포넌트에서는 전역으로 띄울 수 있는 모달 컴포넌트들이 위치해 있었습니다 저는 주스탄드를 사용하여 모달을 띄우기 때문에 모달을 종류별로 하나씩 띄울 수 있게 페이지 컴포넌트에 묶어 놨었습니다.

그런데 팀원분의 페이지 컴포넌트를 확인해보니, 모달컴포넌트가 없더라구요? 팀원분의 모달컴포넌트는 모달을 트리거하는 버튼 컴포넌트와 함께 위치시켜놓음으로써 응집도를 높였었습니다. 물론 그러면서 결합도도 증가한다는 점도 있었습니다.

페이지 컴포넌트들 page 계층에 두고있고, 모달컴포넌트를 widget에 두고있는 상황이었습니다. 모달을 트리거하는 버튼 컴포넌트는 feature에 존재했었는데, 모달 컴포넌트를 버튼컴포넌트와 함께 두게 되면 계층도 저절로 정리가 되는 좋은 점도 있었습니다. 물론 모달 컴포넌트 내부에서 두 개이상의 도메인을 사용하고 있다면 widget에 유지시켰겠지만, 그렇지 않은 경우에는 feature로 옮겨서 버튼과 함께 파일링하여 코로케이션 시켰습니다.

// 변경 전
// 페이지 컴포넌트
export const PostsManagerPage = () => {
  return (
    <Card className="w-full max-w-6xl mx-auto">
      <CardHeader>
        <CardTitle className="flex items-center justify-between">
          <span>게시물 관리자</span>
          <AddPostButton />
        </CardTitle>
      </CardHeader>
      <CardContent>
        <div className="flex flex-col gap-4">
          <PostsFilter />
          <PostsTable />
          <Pagination />
        </div>
      </CardContent>

     // 모달 컴포넌트
      <AddPostModal />
      <EditPostModal />
      <AddCommentModal />
      <EditCommentModal />
      <DetailPostModal />
      <UserProfileModal />

    </Card>
  )
}


// 변경 후
// 페이지 컴포넌트
export const PostsManagerPage = () => {
  return (
    <Card className="w-full max-w-6xl mx-auto">
      <CardHeader>
        <CardTitle className="flex items-center justify-between">
          <span>게시물 관리자</span>
          <AddPostButton />
        </CardTitle>
      </CardHeader>
      <CardContent>
        <div className="flex flex-col gap-4">
          <PostsFilter />
          <PostsTable />
          <Pagination />
        </div>
      </CardContent>
    </Card>
  )
}

// 모달 버튼 컴포넌트
export const AddPostButton = () => {
  const addPostModal = useAddPostModal()

  return (
    <>
      <Button onClick={() => addPostModal.open()}>
        <Plus className="w-4 h-4 mr-2" />
        게시물 추가
      </Button>
      <AddPostModal />
    </>
  )
}

3. 기능 유지

테스트코드가 존재하지않고 누락된 기능이 있는 과제인 만큼, fsd를 진행하면서 기능도 제대로 동작시키고 싶었습니다. 4,5주차 리팩토링을 진행해 보면서 테스트 코드 힘을 체감할 수 있었고, 이번 주차에는 제공된 테스트코드가 없어서 직접 테스트 코드를 만들어 진행했습니다.

테스트코드에 대한 지식이 부족하며 테스트코드 작성에 시간투자할 생각은 없었기 때문에 AI를 사용하여 테스트코드를 작성한뒤에 과제를 진행했습니다. 한 파일에 통으로 되어있는 코드를 fsd로 구조를 바꾸는 리팩토링이었기 때문에, 사용자 행동(기능) 테스트에 중점을 두어 통합테스트 코드를 추가하고 진행했습니다.

생각한 거와 조금 달라서 과제 진행시에 조금씩 테스트코드를 수정했지만, 기본기능을 누락시지않고 진행할 수 있었던 점이 무척 좋았었습니다.

아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.

feature와 widget과의 경계

Features와 Widgets의 경계가 여전히 명확하지 않습니다. 예를 들어 "게시물 리스트 + 필터 + 페이지네이션"을 하나의 위젯으로 봐야 할지, 각각을 독립적인 기능으로 분리해야 할지 판단하기 어려웠습니다. FSD 가이드라인은 있지만, 실제 프로젝트의 복잡도와 팀의 컨벤션에 따라 경계가 달라질 것 같았습니다.

fsd는 하나의 폴더구조이기 때문에 사람마다 다르다는 것은 알고 있습니다. 그렇기에 자신이 생각하는 기준을 정해두고 일관성있게 분류하고 정리하는 것이 맞다는 것을, 이번 과제로 알게 되었습니다.

feature 간 다른 도메인 참조

과제 메인페이지의 postlist는 Post + User(author)의 조합 데이터를 사용하고있습니다. 수강생 분들과 모여서 postlit를 어디서, 어떻게 만들고 조합할지 한참 이야기 했었습니다. entities 뿐만아니라 feature에서는 다른 도메인을 참조 하지 않는 것을 규칙으로 생각했기 때문에 widget에서 조합하자! 라는 결론이 많이 나왔었습니다.

하지만, 테오의 평일 QnA시간에서 feature에서의 다른 도메인 참조는 막지는 않는다. 상관이 없다. 라는 말을 들으며 제 fsd 세상이 붕괴됐습니다. 하하

// API 레이어 중복

entities/post/api/           // 기본 CRUD
features/add-post/api/       // 게시물 추가 특화 로직
widgets/posts-with-users/api/ // 복합 데이터 조회

이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.

지금 저희 회사는 폴더구조 뿐만아니라 컨벤션도 제대로 되어있지않습니다. 때로는 설계 구조도 잘못되어있어서 예측하는 기능대로 동작하지 않을 때도 있습니다.

이런 부분을 원인분석해서 문제가 발생하는 지점을 찾아내어도, 어떤 구조로 해결할지가 제일 고민이었습니다. 클린코드 주차를 지나며 생각해보니 두 가지의 요소의 부재였던 것 같습니다.

  1. 명확한 계층 분리
  2. 단일 책임 원칙

계층 분리과 명확하지 않았기 때문에 의도치않은 사이드 이펙트를 발생시키기 쉬웠습니다. 하나의 목적을 설계 되었으니, 다른 이유로도 수정이 일어날 수 있는 상황이었기에 단일 책임 원칙이 위배 되면서 순수한 기능으로는 동작하지 못했었습니다.

물론, 해당 설계 코드는 프로젝트 마이그레이션을 하면서 설계구조도 달라지겠지만 추후에 이와 같은 문제에 대해서 좀 더 다양한 관점으로 볼 수 있는 힘을 기르게 되었습니다.

챕터 셀프회고

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

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

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

해당 주차에서 과제를 진행할 때에는 클린코드로 리팩토링하겟다는 것이 아닌, 더 나은 설계구조로 변경하겠다는 잘못된 방향으로 진행했습니다. 그러다 보니, 1챕터에서 진행한 과제를 복습하며 리팩토링해보는 재미는 있었지만 과제 출제자의 의도를 파악하고 과제를 온전히 느끼고 즐기지는 못했던 것 같습니다.

더티코드를 지양하고 클린코드를 지향하는 것은 의도를 분명히 하는 것임을 깨달았습니다. 코드를 작성하는 것 보다 코드를 읽는데에 많은 시간을 소요하는 개발자 입장에서는 var와 let 로는 예측할 수 없는 위험을, 변수명과 구획화 하는 등과 같이 보다 쉬운 맥락 이해를 돕는다고 생각합니다.

코드를 더듬더듬 읽어가며 흐름을 읽는 명령형 프로그래밍 보다는, 명확한 의도가 드러나는 선언형 프로그래밍이 클린코드의 축에 더 가까운 것 같습니다. 물론 깔끔하고 명확한 것이 클린하다의 기준은 아니지만, 코드는 프로그래머의 작업물이자, 문서화, 소통방식 중 하나라고 생각하기 때문입니다. 어쩌면 자세한 순차적이 설명이 중요할 때도 있지만 흐름을 읽을때는 대체로 명확한 것이 좋다고 생각하기 때문입니다.

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

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

디자인 패턴을 사용해 볼 생각을 별로 하지 못했습니다. ㅠㅠ

그래도 데이터 > 계산 > 액션을 기준으로 잡고 진행했습니다. model > service > hook > view 의 흐름으로 만들어서 원하고자 하는 바를 이루고자 하였지만, 지엽적인 사고와 시야 때문에 도메인 중심에 응집도 뿐만 아니라 역할도 거대하게 가지고있는 수수수퍼 훅을 만들기도 했습니다.

어쩌다 보니 거대한 단일 컴포넌트를 거대한 여러개의 커스텀 훅으로 만들었던 터라, 채점하는 코치님이 당황했을지도 모를일 입니다. 하하하 훅과 컴포넌트 분리하는 시간이 부족하여 얼레벌레 진행하게 되어, 기준이 있는 것도 아니고 깔끔하지도 않고 요상한 구조가 되었기 때문입니다.

컴포넌트가 독릭적이 되어가는 과정을 깨닫지는 못했어서 무척 아쉬운 주였습니다. 도메인 주도 설계가 무엇인지 더 알게 되었고, 제 과제코드를 통해 응집도가 높으면서 결합도 까지 높아버리는 최악의 워스트 케이스도 알았던 것을 생각하면 더 많은 것들을 배우고 깨달았던 과제 였던 것 같습니다.

다시보니, 과제의도와 반대로 진행을 했었군요? 항상 과제 의도를 빨리 파악하고 시간내에 과제를 완료하는 것을 목표로 삼고있었는데 지켜진 적인 없어서 아쉬울 따름이네요. 하하하!

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

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

이번 주차가 시작되면서 과제에 대해서 스터디원 분들과 많은 이야기를 나눌 수 있었던 것이 제일 즐거웠습니다. fsd는 결국 나의 생각과 기준이기 때문에 사람마다 다른 fsd 구조가 나올수 밖에 없더라구요.

하나의 과제를 가지고 여러 사람들과 함께 코드 위치에 대해서 고민해보고, 자신의 생각을 공유하는 시간은 값졌습니다. 같은 생각을 하고 있으면 반갑기도 하면서도 다른 생각을 하고 있으면, 어떤 이유와 관점으로 그런 방향이 도출되었는지 이야기하는 것이 재밌었습니다. 물론, 재미랑 과제 진도는 조금 달라서 과제 완성에 일정이 벅찼던게 참 아쉽지만요.

어떤 기준으로 폴더를 분리하고 코드 위치를 설정하면서, 머릿속에서는 데이터 구조를 그리기도 하고 이유와 근거에 대해서 깊게 생각했던 것이 좋았스빈다.

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

  • 코치님 이라면 이 과제에서 전역상태 라이브러리를 어느 부분에서 사용하실 것 같나요?
  • 과제에서 게시글 목록 필터의 쿼리스트링을 편하게 관리하기 위해서 커스텀훅을 만들었습니다. 이 훅 구현의 방향성 및 파일 경로에 대해서 궁금증이 있어 질문드립니다.
    • 해당 필터를 조작하는 인터페이스에서는 해당 훅에서 반환한 updateQuery를 가져다 사용하면서 실제 url 쿼리를 업데이트합니다. 그리고 실시간으로 특정 쿼리의 값도 가져올 수 있게 했습니다.
    • 그런데 이게 너무 결합도가 높은 느낌도 받았습니다.(사실 이렇게 표현해야하는지도 잘모르겠네요 ㅎㅎ) 검색어 입력시에는 입력할때마다 url 쿼리가 업데이트되어서 디바운스를 적용해놓았습니다. 이런 방식으로 구현하는것이 나쁘지않은 방식인지 확인 받고싶었습니다.
    • 또한, 쿼리스트링을 직릴화, 역직렬화를 하는 요소가 게시글 목록 api 요소이기 때문에 src/feature/post-query/model/usePostsQuery.ts 의 경로와 이름을 사용했습니다.
    • 그런데 지금 필터 폼 컴포넌트를 파일링해둔 src/feature/posts-filter/ui/PostsFilter.tsx가 존재하는데, 이 것과 같이 파일링하는 것이 좀 더 올바른 방향인지 궁금합니다.
    • 필터에 의해 쿼리값이 나오지만, 쿼리값을 다양한 컴포넌트에서 사용하다보니 다른 슬라이스로 만들었는데 이게 맞는지 고민이 듭니다.

과제 피드백

안녕하세요 지현님! 클린코드 마지막 과제 잘 진행해주셨네요!

코치님 이라면 이 과제에서 전역상태 라이브러리를 어느 부분에서 사용하실 것 같나요?

저는 아마 사용하지 않았을 것 같아요 ㅎㅎ 제가 생각하기에 전역상태는 도메인 상태를 다루는 경우에 필요한데, 지금은 이걸 tanstack-query로 다루고 있고 나머지는 context를 사용하면 되지 않을까!? 라고 생각합니다.

혹은 그냥 context 대신에 jotai나 zustand를 사용해봐도 무방해보이긴해요! ui 전역 상태는 context를 사용하는 편이라..

과제에서 게시글 목록 필터의 쿼리스트링을 편하게 관리하기 위해서 커스텀훅을 만들었습니다. 이 훅 구현의 방향성 및 파일 경로에 대해서 궁금증이 있어 질문드립니다.

일단 디바운스 적용한건 좋은 것 같아요!

지금 제일 큰 고민이 query의 기능이 post에서 쓰이는데 이걸 합치는게 좋을지 안 좋을지에 대한 고민인 것 같네요 ㅎㅎ 저는 좋다고 생각해요! 일단 usePostsQuery 라기보단 usePostFilter 라는 이름으로 사용해주면 어떨까요? 다만 이 filter가 query랑 연계되어있는거죠 ㅎㅎ

const searchQuery = useSearchQuery();
usePostFilter({ onUpdate: searchQuery.update })

그리고 이런 모습을 상상해봐도 좋답니다!