Skip to content

Conversation

@Jihoon-Yoon96
Copy link

@Jihoon-Yoon96 Jihoon-Yoon96 commented Dec 9, 2025

과제 체크포인트

링크 : https://jihoon-yoon96.github.io/front_7th_chapter3-3/#/?limit=10&sortOrder=asc

기본과제

목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 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가 정상적으로 작동하는가?

최종과제

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

과제 셀프회고

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

->구조 설계 : 편연함과 익숙함에 지나쳐왔던 과거를 돌아보며

  • 기존에는 "역할 중심적 구조"를 자주 사용해왔습니다. 사실, 제가 직접 그런 구조를 짠게 아니라 라이브리에서 권장하거나 패키지 설치 시 자동으로 만들어 주는 구조를 그냥 사용해왔던게 가장 정확한 듯 하네요. 공식 문서에서 권장하기도 하고, 이런 구조를 따를 때 해당 라이브러리의 기능을 더 누릴 수 있기도 하고(ex. Nuxt.js의 pages 디렉토리 오토 라우팅), 그리고 무엇보다 이런 구조에서 "이거 불편한데.."라는 생각을 잘 못느껴서.. 더 정확히는 관심사가 여기저기 흩어져 있을 떄의 불편함이 구조를 수정해야된다는 생각까지 이어지지 못했던 것 같습니다.
  • 사실 '불편함'은 이번에 FSD구조에 대해 설계해보면서 더 크게 느껴던 것 같습니다. '기능'을 중심으로 레이어를 나누는 과정에서 "이건 entities야 features야..?", "이건 shared로 쪼개? 아님 그냥 widgets으로 뭉뜽그려..?" 와 같은 고민의 과정이 많았습니다. "학습자"로써 접근했을 때는 이러한 고민의 과정은 스스로도 재밌고 유익했지만, "실무자"로써는 이런 고민,토론이 잦게 발생한다는 것 자체가 FSD구조가 과연 효율적인 구조일까?에 대해 의문을 품게 했습니다. 제가 낯선 구조를 접하고 있어서일 수도 있지만, 발제 시간에도 언급됐듯 "좋은 구조는 누가봐도 명확해야한다."라는 말에 공감하기 때문에, features인지 entities인지, shared로 보낼지 widgets으로 뭉칠지 등에 대한 고민이 빈번히 발생한다는 건 모두에게 명확하지 못하다 생각했습니다.
  • 하지만 멘토링을 통해 이러한 의구심을 어느 정도 해소할 수 있었습니다. '좋은 구조는 모두에게 명확해야 한다'는 명제에서 '모두'의 범위를 불특정 다수가 아닌 '함께 일하는 동료'로 좁혀 생각해보니 해답이 보였습니다.
    FSD의 단점인 높은 자유도와 모호함은, 팀원 간의 의견을 조율하는 장치를 통해 걷어내고 장점만 취할 수 있기 때문입니다.
    이번 과제에서 docs/architecture.md와 같은 레이어 정의 가이드 문서를 작성해 본 것이 그 시작이었습니다. 초반에는 레이어의 경계에 있는 애매한 기능들을 정의하는 것이 어렵고 시간도 걸리지만, 한 번 기준이 확립되고 나면 팀원 모두의 이해도가 통일되어 이후에는 불필요한 고민 시간을 획기적으로 단축할 수 있다는 것을 알게 되었습니다. 결과적으로 FSD가 모든 상황의 정답은 아닐지라도, 우리가 개발하는 서비스의 핵심 기능(Feature)들을 응집도 있게 모아두는 방식이 장기적인 유지보수와 협업 관점에서는 훨씬 효율적이고 편리한 구조라는 확신을 갖게 되었습니다.
  • 돌이켜보면, 그동안은 익숙한 패턴과 프레임워크가 제공하는 편안함에 젖어 '현재 구조가 최선인가?'라는 본질적인 의문을 품지 못했습니다. 하지만 이번 과제를 수행하며 "관심사의 분리"가 왜 필요한지, 그리고 "낮은 결합도와 높은 응집도"를 갖춘 구조가 유지보수에 어떤 영향을 미치는지 몸소 부딪히며 깨달을 수 있었습니다. 단순한 구현을 넘어 '지속 가능한 소프트웨어 구조'에 대해 깊이 고민해볼 수 있었던, 개발자로서 한 단계 성장하는 뜻깊은 시간이었습니다.

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

이 코드가 어느 레이어에 속해야 하는가?를 판단하는 데 가장 많은 공을 들였습니다. 특히 entities와 features의 경계를 나누는 것이 어려웠는데, 다양한 FSD관련 문서들을 참고해가면서 AI와 함께 나름의 레이어 정의 문서를 작성해보고, 이를 토대로 분리작업을 해봤습니다.
아래는 문서 내용 중 일부인데, 아래 내용을 그대로 따르기 보다는 개발 과정에서 그때그때 필요하다 "이게 맞다" 싶은 방향대로 작업했습니다.
특히, "단방향 의존성 규칙"을 지키기 위해 리팩토링 순서도 [shared -> entities -> features -> widgets] 순으로 작업한다 정의내렸지만, 작업 과정에서 중간중간 수정이 필요하거나 다음 작업에 필요하다 생각되는 것들은 우선적으로 작업하기도 했습니다.


1. shared

  • 역할: 앱의 가장 기본적인 구성 요소를 모아두는 곳입니다. 특정 비즈니스 로직에 의존하지 않는, 프로젝트 전반에서 재사용되는 코드를 포함합니다.
  • 구성:
    • ui/: 재사용 가능한 UI 컴포넌트 (e.g., Button, Input, Modal)
    • api/: API 클라이언트 인스턴스, 공통 요청/응답 처리 함수 등
    • lib/: 순수 헬퍼 함수, 유틸리티 (e.g., formatDate, cn)
    • assets/: 이미지, 폰트 등 정적 자원
  • 특징: 슬라이스 없이 세그먼트로만 구성됩니다. 다른 어떤 레이어에도 의존하지 않습니다.

2. entities

  • 역할: 프로젝트가 다루는 핵심 비즈니스 데이터(명사적 개념)와 관련된 코드를 그룹화합니다. 기획이 변경되어도 쉽게 변하지 않는 안정적인 코드입니다.
  • 예시 슬라이스: post, user, comment
  • 구성 (슬라이스 내부):
    • model/: 데이터 타입(*.types.ts), 상태 관리 로직(store), 정규화 로직 등
    • ui/: 해당 데이터를 표현하는 순수 UI 컴포넌트 (e.g., PostCard, UserAvatar)
    • api/: 해당 데이터를 다루는 API 함수 (e.g., getPostById, updateUser)
  • 의존성: shared 레이어만 참조할 수 있습니다.

3. features

  • 역할: 사용자에게 실질적인 가치를 제공하는 동작(동사적 개념)을 구현합니다. 사용자의 액션과 관련된 비즈니스 로직을 포함하며, 기획 변경에 따라 수정될 가능성이 높습니다.
  • 예시 슬라이스: sort-posts, add-to-cart, authenticate-user
  • 구성 (슬라이스 내부):
    • ui/: 기능을 수행하기 위한 UI 컴포넌트 (e.g., SortPostsButton, AddToCartForm)
    • model/: 기능과 관련된 상태 관리 로직
    • api/: 기능을 수행하기 위한 API 연동 코드
  • 의존성: shared, entities 레이어를 참조할 수 있습니다.

4. widgets

  • 역할: 여러 entitiesfeatures를 조합하여 만드는, 독립적으로 작동하는 대규모 UI 블록입니다. 페이지의 특정 구역을 담당하며, 여러 페이지에서 재사용될 수 있습니다.
  • 예시: Header, Footer, PostsList, Sidebar
  • 특징: 자체적인 비즈니스 로직을 거의 가지지 않고, 하위 레이어의 기능들을 엮어주는 역할을 합니다.
  • 의존성: shared, entities, features 레이어를 참조할 수 있습니다.

5. pages

  • 역할: 애플리케이션의 실제 페이지를 구성하는 진입점입니다. 라우팅 설정에 따라 렌더링되며, 주로 widgetsfeatures를 조립하여 최종 화면을 만듭니다.
  • 예시: PostsManagerPage, HomePage, UserProfilePage
  • 특징: 페이지 단위의 레이아웃을 정의하고, 필요한 데이터를 불러오는 로직을 트리거할 수 있습니다.
  • 의존성: shared, entities, features, widgets 레이어를 참조할 수 있습니다.

6. app

  • 역할: 애플리케이션 전체에 영향을 주는 코드를 관리합니다. 앱의 초기 설정, 전역 스타일, 라우팅, 프로바이더 등을 포함합니다.
  • 구성:
    • providers/: 라우터, 전역 상태 관리자(Store Provider), 테마 등 앱 전체를 감싸는 컴포넌트
    • styles/: 전역 CSS, reset.css 등
    • main.tsx / App.tsx: 애플리케이션의 진입점
  • 의존성: 프로젝트의 모든 레이어를 참조할 수 있습니다.

추가로,
features 레이어를 분리하는 과정에서 수많은 기능이 평면적으로 나열되어 가독성이 떨어지는 문제를 발견했습니다(ex. 게시글 추가를 수정하려면 /post/api도 가고 /post/ui도 가야하고..). 이를 해결하기 위해 /features/{도메인}/{세부기능}/{세그먼트} (예: features/comment/add/ui) 형태로 뎁스를 한 단계 더 추가하여 구조화했습니다. 덕분에 CRUD 등 연관된 기능들이 흩어지지 않고 응집도 있게 관리되어, 파일 탐색과 유지보수가 훨씬 수월해졌습니다.

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

FSD가 대규모 프로젝트에는 유용해 보이지만,
작은 기능 하나를 추가할 때도 여러 폴더를 오가며 파일을 생성해야 하는 점이 오버헤드처럼 느껴지기도 했습니다.
또한, shared/ui의 컴포넌트들이 비즈니스 로직에 전혀 의존하지 않도록 완벽하게 순수하게 유지하는 것이 생각보다 까다로웠습니다.
(공통 소스로 분리하고 싶은 것들이 정말 많았지만, 고민의 과정이 너무 길어져 시간도 부족했고.. 아직 FSD구조에 적응 중이라 어느 레이어에 각각의 기능들을 배치시켜야할지 감이 안와 진행하지 못했습니다.. (ex. 팝업 공통화 너무 하고 싶었습니다 ㅠ..))
프로젝트 규모에 따라 FSD의 규칙을 얼마나 엄격하게 적용해야 할지, 혹은 유연하게 가져갈지에 대한 고민이 더 필요할 것 같습니다.

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

챕터 셀프회고

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

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

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

처음 제공된 더티 코드를 봤을 때는 모든 로직이 한 파일에 뭉쳐 있어 어디서부터 손대야 할지 막막했습니다. 하지만 변수명을 명확히 하고, 함수를 단일 책임 원칙에 따라 분리해 나가면서 코드가 점차 읽기 편해지는 것을 느꼈습니다. 유지보수하기 좋은 코드란 단순히 짧은 코드가 아니라, "수정이 필요할 때 어디를 고쳐야 할지 바로 알 수 있는 코드"라는 것을 깨닫게 되었습니다. 그리고 명확한 관심사 분리를 통해 "해당 소스가 어디에 있는지" 쉽게 파악할 수 있게 하는 것도 이번 과제를 통해 느끼게 됐습니다.

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

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

단일 컴포넌트(PostManagerPage.tsx)를 분해를 시작할 때는 분리해야할 관심사가 많아보여 막막했슶니다. 레고 블록을 해체해서 다시 종류별로 정리하는 느낌...? 일단 시작으로 UI 컴포넌트(shared/ui)를 먼저 분리하고 유틸함수를 쪼갠 다음 entities를 정립해보니 그때부터 "뭘 해야하는구나!"라는 방향성이 잡힌 것 같습니다.
또한, 리팩토링 과정에서 Props Drilling 지옥에서 벗어나면서 컴포넌트 간의 불필요한 결합도를 낮출 수 있었습니다.

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

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

"이 API 호출 함수는 어디에 둬야 하지?"라는 고민이 많았는데, 단순히 FSD의 계층을 세 개로만 쪼개는 것이 아닌 세부 단계를 하나 더 배치하면 어떨까?의 생각해보면서 나만의 구조를 잡아갔던 것 같습니다. "/features/{슬라이스}/{세부기능(CRUD 등)}/세그먼트" 이런 구조로 설계를 해보면서 API들을 잘게 쪼개보았고, 개인적으로 가독성이 더 좋아졌다 생각하는 부분입니다. 특히, anStack Query 도입 후에는 컴포넌트 내부에서 useState와 useEffect로 범벅이 되었던 서버 데이터 관리 로직이 사라지고, 데이터 캐싱과 동기화 로직이 깔끔하게 정리될 수 있었다 생각합니다.

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

  1. 낙관적 업데이트를 적용해보긴 했는데, 제대로 한게 맞나 싶긴 하네요.. 낙관적 업데이트 프로세스대로 UI 선 반영하고 목록API 다시 받아와서 그리는 로직을 구현했습니다. 근데 확인한 바로는 CRUD를 해도 API 리스폰스에는 반영되지 않는 문제가 있어서 화면 상으로는 제대로 반영되지 않은 것 처럼 보이고 있습니다..! (그나마 local에서 실행하면 깜빡거리기는 함..) 리스폰스가 정상적으로 반환된다는 전제 하에, 이렇게 처리하는게 낙관적 업데이트가 맞는지 궁금합니다!

  2. 시간이 없어 직접 구현하진 못했지만, 팝업 호출부를 공통화하여 글로벌하게 관리하고 싶다는 생각을 했습니다. 이와 관련하여 막연하게 계획한 내용에 대해 피대븍 주시면 감사하겠습니다..!
    (두서없이 적어 이해하기 힘드실 수도 있지만.. 최대한 자세히 적어보겠습니다..!)

app.tsx에 CommonModalLayouts 컴포넌트 배치

  • Dialog 헤더랑 푸터는 고정
  • Body영역은 props로 컴포넌트를 통으로 받아 배치
  • 모달 전역 상태값 배열 길이가 1 이상일 때 팝업 노출됨

useModal이라는 전역 상태관리 시스템 구축

interface ModalState {
  id: number;
  component: React.ComponentType<any>;
  title: React.ReactNode;
  props?: any;
  close: (result?: any) => void;
}

interface Modals {
  modals: ModalState[];
}

const initialState = {
  modals: []
}

const openModal = (state, action) => {
  console.log('openModal action', action);

  state.modals.push(action.payload);
}
const closeModal = (state, action) => {
  state.modals = state.modals.filter((modal) => modal.id !== action.payload);
}

export const openModalWithPromise = (modalProps) => {
  return new Promise((resolve) => {
    const id = Math.floor(Math.random() * 10000);
    
    const close = (result?: any) => {
      resolve(result);
      dispatch(closeModal(id));
    };

    openModal({ id, ...modalProps, close });
  });
};
  • 초기데이터 modals가 배열인 이유는 "댓글 추가" 팝업처럼 이중 모달을 구현해야 하기 때문 (아래 인터페이스 참고)

openModalWithPromise 프로세스 부연 설명

import { AddCommentDialog } from "../../../features/comment/add/ui/AddCommentDialog"

export const CommentListWidget = () => {
  ...
  async function openEditCommentDialog() {
    const result = await openModalWithPromise({
      component: AddCommentDialog,
      title: '새 댓글 추가',
      props: { open, postId } //
    });

    if (result) {
      setForm({ ...form, address: result });
    }
  }

  return (
    <Button size="sm" onClick={() => openEditCommentDialog()}>
      <Plus className="w-3 h-3 mr-1" /> 댓글 추가
  </Button>
  )
}

이런 식으로 특정 컴포넌트에서 useModals의 상태값이 변경됨.

  • Promise로 비동기 처리를 한 이유
    • CommonModal.tsx가 각각의 컴포넌트에서 호출되는게 아니라 App.tsx에서 하나로 관리되기 때문임
    • AddCommentDialog은 이중 모달 구초로 오픈되는 프로세스여서, 기존 모달창에 입력된 댓글 값을 전달해줘야 하는데, 이를 위한 변수 생성 없이 reslove로 처리하여 받게 하기 위함

--> 진짜 딱 여기까지 밖에 구상이 안되고.. 이거를 FSD로 쪼갠다면 어떻게 해야할지 감이 잘 안오더라구요..! 두서없이 적었지만 이해하신대로 피드백 주시면 감사하겠습니다!!

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.

1 participant