프로젝트에서 FSD 구조를 사용해보고 느낀 실패담

ChoiYongHyeun·2025년 1월 20일
3

리액트

목록 보기
33/37
post-thumbnail

장장 4개월에 거친 프로젝트를 끝마쳤다.

처음에는 단순 본격적 프로젝트 시작 전 맛보기 느낌으로 2개월 정도만 진행합시다 ! 했던 프로젝트가

열정있는 디자이너분과 백엔드분이 모집 되면서 기획 및 디자인의 완성도가 높아졌고

그러면서 기간이 꽤나 길어졌었다.


프로젝트를 구할 때 나같은 뉴비는 매번 뽑아주지 않아 서러워서 직접 모집해서 진행했었다...

이번 프로젝트에서 많은 경험을 해봤지만서도 그 중 가장 큰 수확은 요즘 뜨거운 감자인 FSD[1] 구조를 경험해봤다는 것이다.

FSD 구조에 대한 설명은 이미 너무 잘 설명해주고 있는 게시글이 많기에 레퍼런스만 남겨두고 실패로 얼룩진 내 FSD 사용 경험에서 아쉬웠던 점들을 이야기 해보려 한다.

참고하면 좋은 FSD 레퍼런스
Welcome | Feature-Sliced Design -FSD 공식 문서[1]
FSD 아키텍처 알아보기[2]

재사용성을 고려하지 않고 레이어에만 집중했던 점

FSD 구조의 가장 큰 특징은 아무래도 레이어일 것이다.

그 중 가장 헷갈리게 했던 것은 entitie , features 레이어인데 저 두 레이어가 헷갈리는 이유는

도메인 성격을 가진 slice 를 가지기 시작하는 레이어기에 특정 도메인에 담길 컴포넌트가 있다면 그 컴포넌트가 entities , features 레이어 중 도대체 어느 레이어에 넣어야 할지에 대해 고민해야 했기 때문이다.

우리는 우선적으로 props 를 받고 내부에 비즈니스 로직이 존재하지 않는 컴포넌트를 entities 레이어에, 내부에서 하나라도 비즈니스 로직이 존재하는 컴포넌트를 featues 레이어에 두기로 결정하고 우선 진행했다.

그래서 우리는 컴포넌트를 생성하기 전 아 이 컴포넌트는 어디가 좋지? 비즈니스 로직 없이 props를 받아 렌더링만 하니 entities 에 넣어야겠다 이런식의 사고방식을 먼저 하고 진행했다.

// entities/profile/ui 의 일부 

export const MyProfileOverview = ({
  nickname,
  pet,
  followersIds,
  followingsIds,
}: ProfileOverviewProps) => {
  const { profile, name, breed, description, personalities } = pet;

  return (
    ...
  );
};

예를 들어 위의 커다란 하나의 컴포넌트는 서버로부터 유저의 정보를 받아 마이 페이지에서 정보를 보여주는 컴포넌트이다.

사실 FSD 구조상으로 보면 entities 레이어에 존재하는 것이 맞아 보이지만

문제는 저 컴포넌트가 마이 페이지 외에선 사용되지 않는, 재사용이 하나도 일어나지 않는 컴포넌트였다.

그럼 한 파일에서만 사용하는 컴포넌트를 굳이 entities 레이어에서 다른 파일로 만들어 놓고 사용 할 필요가 전혀 없었다고 생각한다.

파일 개수가 늘어나면 늘어날 수록 신경써야 할 것이 많아지고 사용처와 생성된 위치가 다르다보니 저 컴포넌트를 찾으려면 저 컴포넌트가 페이지 컴포넌트를 찾고, 그 페이지에서 저 컴포넌트가 있는 파일까지 이동했어야 했다.

저 문제가 더욱 심각했던 것은 featues 레이어였다.

예를 들어 위 처럼 폼 양식을 가진 컴포넌트는 내부에서 api 요청부터 상태관리까지 다양한 로직이 존재하기에 features 레이어에 있기에 무리가 없다.

하지만 비밀번호 변경 모달이나, 수정, 설정 등과 같이 특정 페이지에서만 사용 되는 모달들 까지 굳이 featues 레이어에 놔뒀어야 했나라는 후회가 든다.

하나의 파일 (페이지) 외에서는 다른 곳에서 사용되지 않는 파일들은 차라리 해당 파일이 선언된 곳에서 생성해줬다면 어땠을까 ?

이렇게 하면 생성된 곳과 사용하는 곳이 동일하여 결합도가 훨씬 올라갔을 것이며

featues 레이어에선 비즈니스 로직이 가미되고 재사용성이 있는 컴포넌트들만 모여져있었을 것이다.

사용 할 때 결합도를 생각치 않고 이게 어떤 레이어에 있는게 적합하다! 라는 결론만을 내린채 섣불리 위치시켰던 것이 패착이였다.

너무나도 느끼는게 한 폴더 안에 파일 개수가 늘어나면 늘어날 수록 종잡을 수 없이 더러워지고, 그 폴더가 의미하는 바를 잘 나타내지 못하게 되는거 같다

도메인들을 잘 나누지 못했던 점

뭐 이게 아무래도 FSD 구조를 사용 할 때 가장 골머리를 아프게 했던 부분인 거 같다.

FSD 는 특성상 같은 레이어에선 다른 도메인 (slice) 의 파일을 참조하면 안된다. 만약 그래야 한다면 상위 레이어인 widget 레이어에서 참조해야 한다고 이야기 한다.

진행했던 프로젝트에선 유사하지만 나뉘어진 도메인들이 두 가지 쌍이 존재했다.

말 그대로 지도 그 자체를 나타는 map 도메인과 게시글등을 지칭하는 marking 이란 도메인,

유저의 프로필 정보를 나타내는 profile 도메인, 유저의 인증 정보를 지칭하는 auth 도메인, 유저의 팔로잉 팔로워 정보를 지칭하는 follow 도메인 , 유저의 정보 셋팅값을 지칭하는 setting 도메인 ..

저 도메인들은 사실상 map 이라는 도메인과 user 라는 하나의 도메인으로 묶어버릴 수 있을 정도로 관계가 깊었다.

관계가 깊은 도메인들을 굳이 나뉘었던 이유는 아마 FSD 구조에 대해 이해 없이 작업하던 중

인증 관련 정보를 작업 할 땐 auth 라는 도메인을 만들고 그 다음 프로필 정보를 작업 할 땐 profile 이라는 도메인을 만들고 하다보니

저렇게 애매모호한 도메인들이 나타나게 되었다.

저렇게 관계가 깊은 도메인들은 자주 서로 도메인 내에 정의 된 api 요청이나 타입들을 참조해야 했고

같은 레이어 내부 다른 도메인의 정보를 참조하면 안되는 FSD 구조의 특성상

cross import[3] 라는 방법을 이용해서 이 문제를 해결하고 했다.

// @/entities/marking 에 해당하는 특정 파일 중 하나 
...
import { LatLng } from "@/entities/map/@x/marking";

cross import 방식은 FSD 공식 문서에서도 소개하는 방법이긴 하나 저 방식은 FSD 구조의 장점을 살리지 못하는 최후의 수단인 느낌이다.

지금 와서 생각해보면 단순히 user , map 두 가지 도메인으로만 작업했더라면 , 아니면 더 도메인들을 잘 묶어서 작업했더라면 저런 불상사는 일어나지 않았을 것이다.

사실 cross import 를 쓰는 것이 잘못됐다는 것은 아니다. 도메인이 애매모호하면 생기는 가장 큰 문제는 파일을 어느 위치에 위치시킬지 결정하기도 힘들고, 찾기도 힘들다는 것이다. 그것도 매우!!!

위젯 레이어를 이상하게 쓴 점

FSD 구조를 경험하기 전 처음에는 위젯 레이어를

ㅋㅋ 아 약간 컨테이너 같은 느낌?의 컴포넌트인가보다. children 받는 어떤 레이아웃으로 정해놔야지 ~ 이런 생각으로 이딴 컴포넌트를 만들었다.

// widgets/marking/ui 파일 중 일부 

export const MarkingList = ({
  children,
  className = "",
  display = "list",
}: ContentListProps) => {
  const markingListStyles =
    display === "grid"
      ? "grid grid-cols-3 rounded-lg overflow-hidden"
      : "flex flex-col gap-8";

  return <ul className={`${markingListStyles} ${className}`}>{children}</ul>;
};

공식 문서에선 widget 레이어를 다음처럼 설명한다.

Widgets - 독립적으로 작동하는 대규모 기능 또는 UI 컴포넌트, 보통 하나의 완전한 기능. [4]

widget 레이어는 단순히 칠드런만 받아서 렌더링 하는 컴포넌트가 아닌

서로 다른 레이어 및 도메인들의 조합으로 이뤄진 완성된 컴포넌트 역할을 하는 것이 올바를 것이다.

// widgets/marking/ui 파일의 일부 
// 이런식으로 작성해야 했을 것이다

import { useState, useRef, useEffect } from "react";
import { FollowingToggle } from "@/features/follow/ui";
import { useDeleteMarking } from "@/features/marking/api";

...
export const MarkingItem = (props: MarkingItemProps) => {
  ... 여러 컴포넌트들의 조합으로 이뤄진 거대한 컴포넌트
 }

그갸갹 왜 저따구로 만들었었지..

API 들을 여러 레이어에 나눠 만들었던 점

우리는 api 요청들을 다음과 같은 기준으로 레이어에 나눠 담았다.

ㅋㅋ entities 레이어는 뭔가 비즈니스 로직 없이 props 로 값을 받기만 하니 약간 수동적?인느낌? 그럼 GET요청은 모두 entities 레이어 렛츠고

ㅋㅋ featues 레이어는 비즈니스 로직이 내부에 존재하니 약간 능동적?느낌? 그럼 POST,PUT,DELETE 요청은 모두 feastues 레이어 렛츠고

api 들을 단순히 이렇게 나누게 되면 발생했던 문제점이 존재했는데

타입들을 재사용하기가 더럽게 힘들다는 점 이다.

FSD 는 하위레이어가 상위 레이어의 값을 참조 할 수 없다.

위 처럼 나누게 되면 entities 레이어 존재하는 컴포넌트들은 props 로 POST,PUT,DELETE 등의 api 요청과 관련된 타입들을 사용 할 수 없게 되어

울며 겨자먹기로 동일한 타입을 entities 에서 생성하거나, widget 레이어로 그 컴포넌트를 옮겼던 기억이 난다.

FSD 문서나 다른 예시들에선 종종 api 요청들을 shared/api/도메인명 에 작성하거나

entities/도메인명/api 에 우다다다 때려놓은 예시를 보곤 했는데

그 이유가 아마 가장 낮은 레이어에 api요청들을 둘 수록 타입들을 어느 레이어에서든 참조 할 수 있기 때문이 아니였을까? 라는 생각이 든다.


회고

사실 이번 플젝에서 사용한 FSD 는 점수로 따지자면 100점 만점에 55점 정도를 줄 수 있을 거 같다.

좋진 않지만 최악은 아니였던 ..

그럼에도 불구하고 FSD 구조는 여전히 매력적인 거 같다. (내가 이상하게 써서 그렇지)

확실히 components , hooks , api 이런 식의 폴더 구조로만 나눌 때 보다

컴포넌트들의 분화도에 따라 (비즈니스 로직 유무) , 도메인에 따라 나누다 보니

프로젝트 자체의 구조가 계층적으로 눈에 그려진다는 것이 확실히 느껴지더라

Nuke App 🦄⚛️[5]

위 페이지는 FSD 를 사용한 페이지에서 FSD 구조에 따라 컴포넌트를 구분한 모습인데

앱의 구조 자체가 한 눈에 그려진다.

하.. 한 번만 더 하면 진짜 잘 할 수 있을 거 같은데 .. 그갸갹 아쉬움이 많이 남는다.

  • 출처
  1. Welcome | Feature-Sliced Design
  2. FSD 아키텍처 알아보기
  3. Cross-imports | Feature-Sliced Design
  4. FSD 공식문서 - widget 레이어에 대한 설명
  5. Nuke App 🦄⚛️
profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글

관련 채용 정보