항해플러스 7기 8주차

드엔트론프·2025년 12월 14일

항해플러스

목록 보기
8/9
post-thumbnail

기능 중심 아키텍처와 프로젝트 폴더구조

이번주 요약

  1. 이번주는 기능 중심 아키텍처와 프로젝트 폴더구조에 대해 공부하는 주차였다.
  2. 프론트에서 기능 중심 아키텍처? 뭘 말하는 걸까? 역시 FSD다.
  3. 평소 FSD에 대한 개념만 들어봤지, 실제로 적용해 볼 일은 없었는데, 이번 주차로 오며 조금은 가까워진 듯 (?)
  4. 아쉽게도 아직까지 기능 중심 아키텍쳐를 제대로 쓰지 못한다 생각하기때문에, 글이 그리 길지는 않음

FSD에 대한 개념을 조금은 이해한 것

  • 앞서 말했듯, 몇몇 글 보면서 그래서 저걸 왜 쓰는데? 오히려 너무 복잡한 것 아닌가? 굳이 필요한가 ? 싶은 개념이었다.
  • 가장 최근 올라온 카카오페이에서의 FSD 글도 보고, 발제 자료도 보고 여러 아티클들을 보면서도 이걸 쓴다고? 이렇게 쓴다고..? 싶었다.
  • 개념이 낯선 부분은 어느정도 친해졌는데, 개념과 별개로 이를 적용하는 것이 어려웠다.

진행 스토리

  • input, button과같은 공통 컴포넌트를 분리하고, 이를 shared/ui에 담았다.
  • api 관련 부분을 entities/post/api, entities/user/api 등으로 구분하였다.
export const postsApi = {
  // 게시물 목록 조회
  getPosts: async (limit: number, skip: number): Promise<PostsResponse> => {
    const response = await fetch(`/api/posts?limit=${limit}&skip=${skip}`)
    return response.json()
  },

  // 게시물 검색
  searchPosts: async (query: string): Promise<PostsResponse> => {
    const response = await fetch(`/api/posts/search?q=${query}`)
    return response.json()
  },

  // 태그별 게시물 조회
  getPostsByTag: async (tag: string): Promise<PostsResponse> => {
    const response = await fetch(`/api/posts/tag/${tag}`)
    return response.json()
  },

  // 게시물 추가
  createPost: async (post: NewPost): Promise<Post> => {
    const response = await fetch(`/api/posts/add`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(post),
    })
    return response.json()
  },

  // 게시물 수정
  updatePost: async (id: number, post: Partial<Post>): Promise<Post> => {
    const response = await fetch(`/api/posts/${id}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(post),
    })
    return response.json()
  },

  // 게시물 삭제
  deletePost: async (id: number): Promise<void> => {
    await fetch(`/api/posts/${id}`, { method: "DELETE" })
  },
}
  • entities는 순수한 비즈니스 데이터와 로직을 포함 기획이 변해도 변하지 않을 데이터 기반의 코드들이 있어야한다.

  • 그렇기에 api도 그냥 api의 기본을 적고, 이를 활용하는 건 features에서 적용해준다.

  • 여기 api 뿐만 아니라 ui도 순수하게 그려져야 했다. 그래서 테이블 row를 그리는 컴포넌트도 아래처럼 순수하게 그려주었다.

//src/entities/post/ui/post-table-row.tsx

import { highlightText } from "@/shared/lib/text"
import { TableCell, TableRow } from "@/shared/ui"
import { ThumbsUp, ThumbsDown } from "lucide-react"
import { TagBadge } from "@/entities/tag"
import type { Post } from "../model/types"

interface PostTableRowProps {
  post: Post
  searchQuery?: string
  selectedTag?: string
  onTagClick?: (tag: string) => void
  onUserClick?: (userId: number) => void
  actions?: React.ReactNode
}

export const PostTableRow = ({
  post,
  searchQuery = "",
  selectedTag = "",
  onTagClick,
  onUserClick,
  actions,
}: PostTableRowProps) => {
  return (
    <TableRow>
      {/* ID */}
      <TableCell className="w-[50px]">{post.id}</TableCell>

      {/* 제목 + 태그 */}
      <TableCell>
        <div className="space-y-1">
          {/* 제목 (검색어 하이라이트) */}
          <div>{highlightText(post.title, searchQuery)}</div>

          {/* 태그 */}
          <div className="flex flex-wrap gap-1">
            {post.tags?.map((tag) => (
              <TagBadge key={tag} tag={tag} isSelected={selectedTag === tag} onClick={onTagClick} />
            ))}
          </div>
        </div>
      </TableCell>

      {/* 작성자 */}
      <TableCell className="w-[150px]">
        <div
          className="flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity"
          onClick={() => onUserClick?.(post.userId)}
        >
          {post.author?.image && (
            <img src={post.author.image} alt={post.author.username} className="w-8 h-8 rounded-full object-cover" />
          )}
          <span className="truncate">{post.author?.username || "Unknown"}</span>
        </div>
      </TableCell>

      {/* 반응 (좋아요/싫어요) */}
      <TableCell className="w-[150px]">
        <div className="flex items-center gap-2">
          <div className="flex items-center gap-1">
            <ThumbsUp className="w-4 h-4 text-blue-500" />
            <span className="text-sm font-medium">{post.reactions?.likes || 0}</span>
          </div>
          <div className="flex items-center gap-1">
            <ThumbsDown className="w-4 h-4 text-red-500" />
            <span className="text-sm font-medium">{post.reactions?.dislikes || 0}</span>
          </div>
        </div>
      </TableCell>

      {/* 작업 (버튼) */}
      <TableCell className="w-[150px]">
        <div className="flex items-center gap-2">{actions}</div>
      </TableCell>
    </TableRow>
  )
}

막혔던 부분과 해결

  • 처음 막혔던 부분은 이 부분이다. post와 user를 합친 데이터를 테이블에서 보여줘야한다.
  • entities에서는 서로 공유하면 안되고, 상위 폴더인 features에서 가져와 써야한다.
  • 내가 처음 생각한 구조는 entities에 작성된 table-row를 활용해서 table을 features에서 구현하려했는데, 여기서 구현한 테이블을 pages에서 가져오면 되지 않을까? 했던거다.
  • 그런데 이제 순수하지 않은, 변화와 관련된 로직을 features에서 구현해야한다. 예를들면 글 수정, 삭제같은 로직도 features에 있어야되네? 그러면 너무 큰 테이블 구현은 상위 단계인 widgets 에서 구현해야되나? 나는 widgets 까지는 구현하기 싫은데? 여긴 조금 더 재사용할 수 있는 큰 컴포넌트 위주로 작성돼야하지 않나?
  • 이런 고민을 계속하다보니 시간이 많이 부족해진 것 같다.
  • 결론적으로는 features 에서 delete-button, post-detail-button 등을 구현하고, 전체를 활용하는건 pages 폴더에서 처리하게 됐다.
  • (사실 이 구현이 마음에 들지 않는다.)

KPT

Keep

  • 갑작스러운 끝맺음이지만, FSD를 이해해보려 노력하고 스스로 꽤 많은 고민을 해본점에 대해 칭찬하고싶다.
  • 원하는대로 구현하진 못했지만, 흐름과 내용에 대해 이해할 수 있었다.

Problem

  • 생각이 꼬리에 꼬리를 물다 심화과제까지 끝내지 못했다. 다른 사람들은 어떻게 잘 끝내는지, 내가 너무 느린가 가끔씩 비교돼서 슬픔

Try

  • 퇴근 후 집에 와서 저녁먹고 12시까지 자고 일어나서 새벽에 집중하는 게 더 집중 잘되는 것을 느꼈다. 팀 모임을 못해서 미안하긴하지만,,
  • 이번주는 이렇게 해봐야되나 ?!

중요한 점

  • 이러한 FSD를 온전히 구현하는 것보단 사실 더 중요한 건 아래와 같다.

    최종 폴더 구조가 결합도를 낮추고 응집도를 높이려는 의도를 명확히 보여주어야 합니다. FSD의 규칙을 기계적으로 따르는 것보다, 그 근본 원칙을 이해하고 자신의 프로젝트에 맞게 적용하려는 노력이 중요합니다.

  • FSD가 유행하게 된 건 폴더구조 자체를 멘탈모델로 가져가며, 사람들에게 기준을 제시해준 것이라한다.
  • 이를 이해하고, 무엇이 내 프로젝트에 더 맞는 방법일지를 맞춰가며 결합도는 낮추되 응집도는 높인 폴더 구조와 코드를 가져가는 것이 중요함을 깨달았다.

이번주 항해

  • 팀원, 학메와 보드게임을 하러 갔다. 강남 시네마점 클라스 장난 없음!

  • 중간에 코드리뷰 시간때문에 팀원 한 분이 잠시 빠졌는데, 잠시 아니라 그냥 2시간 없어져버렸음 ㅋㅋ

  • 집 갈 시간인데도 끝나지 않은 코드리뷰 때문에, 노트북 들고 길거리를 돌아다녔다는 후기..

  • 같이 추억 겸 스티커사진도 찍고~, 바쁜 와중에도 이렇게 팀의 우애(?)를 다지는 게 참 좋다.

  • 그래서 이번주 사진은 보드게임 하며 시켰던 치킨 떡볶이~

profile
왜? 를 깊게 고민하고 해결하는 사람이 되고 싶은 개발자

2개의 댓글

comment-user-thumbnail
2025년 12월 15일

벌써 8주차를 쓰셨군요~~!
중간 네트워킹 때 사진을 못찍어서 아쉬워하신 만큼, 매 주 WIL에 사진이 하나씩 들어가 있는 것 같습니다ㅎㅅㅋㅎㄱ
남은 2주도 화이팅하세요👍🏻

1개의 답글