Skip to content

[10팀 홍혜원] Chapter 2-3. 관심사 분리와 폴더구조#68

Open
Wonny-ing wants to merge 19 commits intohanghae-plus:mainfrom
Wonny-ing:main
Open

[10팀 홍혜원] Chapter 2-3. 관심사 분리와 폴더구조#68
Wonny-ing wants to merge 19 commits intohanghae-plus:mainfrom
Wonny-ing:main

Conversation

@Wonny-ing
Copy link

@Wonny-ing Wonny-ing commented May 2, 2025

과제 체크포인트

https://wonny-ing.github.io/front_5th_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가 아닌 선언적인 함수형 프로그래밍이 적절히 적용되었는가?
  • 캐싱과 리프레시 전략이 올바르게 구현되었는가?

과제 셀프회고

이번 프로젝트의 목표 중 하나는 효과적인 상태 관리코드 구조화였습니다. 기존 코드는 하나의 컴포넌트에서 너무 많은 useState를 사용하고 있었고 그로 인해 코드가 점점 복잡한 상태였습니다.

가장 첫번째로 한 작업은 관심사에 따라 컴포넌트를 나누는 것이었습니다.

이때 가장 고민했던 부분은 바로

👉 “상태를 어떻게 분리하고, 어떤 계층 구조로 관리할 것인가”였습니다.

단순히 전역 상태 관리 라이브러리를 도입하는 것보다, 상태의 성격에 따라 적절히 나누고, 각 계층의 책임을 명확히 정의하는 것이 더 중요하다고 느꼈습니다.

그래서 이번에는 FSD 아키텍처를 기반으로 상태를 어떻게 나눌지, 어떤 기준으로 관리할지를 중점적으로 고민했습니다.

👾 기존 상태 관리의 문제점

기존 코드는 PostManager 컴포넌트안에서 모든 상태를 useState를 통해 관리하고 있었습니다.

// PostManager.tsx
const [posts, setPosts] = useState([]);
const [total, setTotal] = useState(0);
const [skip, setSkip] = useState(parseInt(queryParams.get("skip") || "0"));
// ... 수십 개의 useState들

이처럼 하나의 컴포넌트에서 너무 많은 상태를 관리하다 보니, 아래와 같은 문제들이 생겼습니다:

  • Props Drilling
  • 성능 이슈
  • 복잡성 증가
  • 캐싱 부재
  • 코드 중복

이런 문제들을 해결하기 위해, 상태 관리 전략을 다시 설계했습니다.

핵심은 관심사 분리(Separation of Concerns)였고, 다음과 같은 기준을 가지고 리팩토링을 진행했습니다:

리팩토링 접근 방식과 기준

1. 서버 상태와 클라이언트 상태의 명확한 구분

가장 먼저 했던 일은 상태를 두 가지로 나누는 것이었습니다:

  • 서버 상태: 서버에서 가져오는 데이터 (예: 게시글 목록, 댓글, 사용자 정보 등)

    TanStack Query로 관리

  • 클라이언트 상태: UI에서만 의미 있는 상태 (예: 모달 열림 여부, 현재 선택된 항목 등)

    Zustand로 관리

이렇게 구분한 이유는 단순히 기술적인 이유 때문만은 아닙니다!

각 상태는 성격이 완전히 다르기 때문이에요:

서버 상태 (TanStack Query)

  • 비동기로 데이터를 불러오고
  • 캐싱, 리페칭, 에러 처리가 중요하고
  • 네트워크 상태에 따라 UI가 반응해야 합니다

클라이언트 상태 (Zustand)

  • 동기적으로 바로 반영돼야 하고
  • 모달 상태, 필터 선택, 선택된 항목 등 UI 중심의 상태가 많습니다
  • 여러 컴포넌트에서 쉽게 접근해야 합니다

도메인 기반 모듈화

상태를 단순히 기능이나 화면 기준으로 나누는 대신, 도메인 엔티티(Entity) 중심으로 구조화를 했습니다.

즉, 게시물, 댓글, 사용자처럼 실제 비즈니스 개념 을 기준으로 모듈을 나눴습니다!

features/
  post/
    model/
      types.ts      // 타입 정의
      store.ts      // Zustand로 클라이언트 상태 관리
    api/
      queries.ts    // TanStack Query로 서버 상태 관리

이렇게 CommentUser 도메인도 동일한 방식으로 구조화하니까 관련 로직이 한 폴더에 모이면서도 깔끔하게 분리되고 재사용성도 좋아졌습니다.

화면 전용 UI(Layout) 상태 분리

interface LayoutState {
  showAddDialog: boolean
  showEditDialog: boolean
  showPostDetailDialog: boolean
  showUserModal: boolean
  showAddCommentDialog: boolean
  showEditCommentDialog: boolean

  skip: number
  limit: number
  searchQuery: string
  selectedTag: string
  sortBy: string
  sortOrder: "asc" | "desc"

  setShowAddDialog: (show: boolean) => void
  setShowEditDialog: (show: boolean) => void
  setShowPostDetailDialog: (show: boolean) => void
  setShowUserModal: (show: boolean) => void
  setShowAddCommentDialog: (show: boolean) => void
  setShowEditCommentDialog: (show: boolean) => void

  setSkip: (skip: number) => void
  setLimit: (limit: number) => void
  setSearchQuery: (query: string) => void
  setSelectedTag: (tag: string) => void
  setSortBy: (sortBy: string) => void
  setSortOrder: (sortOrder: "asc" | "desc") => void

  initFromURL: (searchParams: URLSearchParams) => void
  updateURLParams: () => string
}

UI 상태는 별도 스토어로 분리했습니다.

이렇게 하니까 전역 스토어를 오염시키지 않으면서도, UI 로직을 깔끔하게 관리할 수 있게 되어 컴포넌트가 깔끔해졌습니다!

서버 상태 vs 클라이언트 상태 구분 기준

TanStack Query로 관리하는 서버 상태

서버에서 오는 데이터를 기반으로 하고, 다음과 같은 특성을 가지면 서버 상태(Server State)로 보고 TanStack Query로 관리했습니다.

  • 데이터를 외부에서 가져오는 경우 (예: API 호출)
  • 상태의 출처가 클라이언트가 아니라 백엔드!
  • 여러 컴포넌트/페이지에서 같은 데이터를 쓰는 경우

📌 예시

  • 게시물 목록 (usePostsQuery)
  • 댓글 목록 (useCommentsQuery)
  • 사용자 정보 (useUserQuery)

Zustand로 관리하는 클라이언트 상태

아래 기준에 부합하는 경우는 클라이언트 상태로 보고 Zustand에서 관리했습니다:

  • UI에서 직접 바뀌는 값들 (선택, 입력, 모달 상태 등)
  • 서버와 무관

📌 예시

  • 현재 선택된 게시물 (selectedPost)
  • 검색 필터 (filters)
  • 페이징/정렬 상태

UI 상태 vs 도메인 상태 구분

클라이언트 상태(Zustand) 안에서도 또 나눴어요:

👉 UI 상태 vs 도메인 상태

UI 상태 (LayoutStore)

순수 UI를 위한 상태도메인 상태와 분리된 전용 스토어로 만들었습니다.

그 이유는 전역 도메인 스토어를 더럽히지 않기 위해서 입니다.

export const useLayoutStore = create<UIState>((set) => ({
  showAddDialog: false,
  showEditDialog: false,
  setShowAddDialog: (show) => set({ showAddDialog: show }),
}));

UI 상태는 단순히 시각적 표현만을 위해 존재하는 상태이기 때문에, Zustand 내에서도 별도 스토어 파일 로 분리하여 관리했습니다!

도메인 상태 (Entity 중심)

게시물, 댓글, 사용자처럼 실제 도메인 모델과 연관된 상태는 전용 도메인 스토어에서 관리했습니다.

  • 모델 상태: 선택된 항목, 임시 입력 데이터 등
  • 도메인 로직에 필요한 상태만 포함
  • 상태 변경 액션은 비즈니스 목적을 기반으로 명확히 정의
// usePostStore.ts
export const usePostStore = create<PostState>((set) => ({
  filters: { searchQuery: '', selectedTag: '' },
  selectedPost: null,
  setFilters: (filters) =>
    set((state) => ({ filters: { ...state.filters, ...filters } })),
  setSelectedPost: (post) => set({ selectedPost: post }),
}));

이런 도메인 스토어는 UI와 독립적으로 테스트 가능해야 한다는 기준도 있었어요!

이렇게 최상위 컴포넌트에서 관리하던 useState를 Zustand와 TanStack Query 기반의 체계적인 상태 관리 구조로 리팩토링하면서, 단순히 상태 관리 라이브러리를 바꾼 게 아니라 애플리케이션 아키텍처 자체를 재설계하는 계기가 되었습니다. 서버 상태와 클라이언트 상태를 명확하게 나누고, 도메인 중심으로 상태와 로직을 모듈화하면서 유지보수성과 확장성이 훨씬 좋아진거 같다고 느꼈어요.

이번 리팩토링을 통해 “상태를 어디에 저장할지”에 대한 고민이 단순한 기술 선택이 아니라 전체 구조와 개발 경험에 영향을 주는 중요한 결정이라는 걸 다시 한번 느꼈습니다!

아직도 잘 모르겠는 부분

features 디렉토리에서 CRUD 관련 컴포넌트들을 따로 분리하긴 했는데, 이미 API 관련 로직들은 대부분 entities 쪽에 정리돼 있어서 어떤 API 로직을 어디에 두는 게 맞는지 명확하게 판단하기 어려웠어요. 예를 들어 entities/post/api/addPost처럼 이미 존재하는 함수가 있는 상황에서, 같은 기능을 features/post/api에도 둬야 하는지, 혹은 옮겨야 하는지 애매하더라고요.
사실 개인적으로는 entities 폴더 하나만 가지고 도메인 기준으로 잘 나눠놓으면 그걸로 충분하다고 느꼈고, 오히려 그게 더 관리하기 편하다고 생각했어요. 이번엔 과제 조건에 맞춰서 features로 나눴지만, 없었으면 그냥 entities 안에서만 관리했을 것 같아요.

…comment 관련 API 호출 로직을 별도의 서비스 파일 생성
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