기능 중심 아키텍처와 프로젝트 폴더구조
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>
)
}
entities에서는 서로 공유하면 안되고, 상위 폴더인 features에서 가져와 써야한다. entities에 작성된 table-row를 활용해서 table을 features에서 구현하려했는데, 여기서 구현한 테이블을 pages에서 가져오면 되지 않을까? 했던거다.features에서 구현해야한다. 예를들면 글 수정, 삭제같은 로직도 features에 있어야되네? 그러면 너무 큰 테이블 구현은 상위 단계인 widgets 에서 구현해야되나? 나는 widgets 까지는 구현하기 싫은데? 여긴 조금 더 재사용할 수 있는 큰 컴포넌트 위주로 작성돼야하지 않나? features 에서 delete-button, post-detail-button 등을 구현하고, 전체를 활용하는건 pages 폴더에서 처리하게 됐다.최종 폴더 구조가 결합도를 낮추고 응집도를 높이려는 의도를 명확히 보여주어야 합니다. FSD의 규칙을 기계적으로 따르는 것보다, 그 근본 원칙을 이해하고 자신의 프로젝트에 맞게 적용하려는 노력이 중요합니다.
팀원, 학메와 보드게임을 하러 갔다. 강남 시네마점 클라스 장난 없음!
중간에 코드리뷰 시간때문에 팀원 한 분이 잠시 빠졌는데, 잠시 아니라 그냥 2시간 없어져버렸음 ㅋㅋ
집 갈 시간인데도 끝나지 않은 코드리뷰 때문에, 노트북 들고 길거리를 돌아다녔다는 후기..
같이 추억 겸 스티커사진도 찍고~, 바쁜 와중에도 이렇게 팀의 우애(?)를 다지는 게 참 좋다.
그래서 이번주 사진은 보드게임 하며 시켰던 치킨 떡볶이~

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