React가 공식 파일 구조를 강제하지 않는 건 장점이야. 대신 규모가 커져도 찾고 옮기기 쉬운 구조를 직접 선택해야 해.
무엇을 기준으로 묶을지 정해
파일을 종류별로 모을 수도 있고 기능별로 모을 수도 있어. 종류별 구조는 모든 훅을 hooks/, 컴포넌트를 components/, 유틸리티를 lib/에 둬. 기능별 구조는 채팅에 필요한 훅과 컴포넌트, 유틸리티를 features/chat/처럼 한곳에 모아.
작은 프로젝트는 종류별 구조가 찾기 쉬워. 기능이 많아지면 기능별 구조가 한 기능을 통째로 옮기거나 삭제하기 편해져. 어느 쪽이든 폴더를 다섯 단계씩 깊게 파기보다 한두 단계에서 파일이 보이게 유지해.
cwkPippa의 프런트엔드
cwkPippa는 기능별 컴포넌트와 공용 훅을 함께 쓰는 혼합 구조야.
src/
App.tsx # 최상위 상태와 라우트
main.tsx # 엔트리
index.css # Tailwind v4와 테마 토큰
components/
chat/ # InputArea, MessageList, MessageItem
sidebar/ # ConversationList, FolderTree
council/ # Council UI
admin/ # 관리 화면
settings/ # 설정 화면
hooks/ # useChat, useConversations, useHeartbeat
lib/ # api.ts, formatter, 상수
types/ # 공용 TypeScript 타입
컴포넌트는 chat, sidebar, council처럼 기능별로 모으고, 여러 기능에서 함께 쓰는 훅과 타입은 종류별 폴더에 둬. 한 프로젝트 안에서도 각 디렉터리의 책임에 맞는 기준을 고르면 돼.
상태를 어디까지 올릴지 정해
cwkPippa의 App.tsx는 여러 자식이 함께 쓰는 상태를 소유하고 props와 callback으로 내려줘. 다른 프로젝트라면 Context나 Zustand store가 그 역할을 맡을 수도 있어. 하나의 정답보다 같은 종류의 상태를 일관된 위치에서 관리하는 게 중요해.
긴 상대 경로는 별칭으로 줄여
../../../components/chat/MessageList처럼 긴 상대 경로는 파일을 옮길 때 쉽게 깨져. @/*를 src/*에 연결하면 @/components/chat/MessageList처럼 읽고 옮기기 쉬운 import를 쓸 수 있어. TypeScript의 paths와 Vite의 resolve.alias를 같은 값으로 맞춰야 해.