Overview
프로젝트 리라이팅 과정에서 겪은 아키텍처 진화 여정을 정리했다. 기존 기술적 역할별 구조의 한계를 느끼고 도메인 중심 구조로 1차 리팩토링을 진행했지만, App Router 환경에서 새로운 고민에 직면하게 되었다. 도메인 응집도와 기술적 최적화를 모두 확보할 수 있는 방법을 찾기 위한 고민과 실험 과정을 기록한다.
처음엔 역할별로 나눴었다
리라이팅 이전 구조는 기술별 분산 구조였다.
기능별로 파일들이 흩어져 있어 하나의 기능을 수정하려면 여러 폴더를 돌아다녀야 했다:
# Before: 기술별 분산 구조 - 리뷰 기능 하나를 위해
/components/common/reviews/reviewcard/ReviewCard.jsx
/components/common/modal/writereview/ReviewModal.jsx
/pages/class/[universityName]/detail/[lectureId].jsx
/api/reviews/index.js
/hooks/useReviewHandlers.js
/stores/useReviewStateStore.js리뷰 관련 버그 하나를 수정하기 위해 6개의 다른 폴더를 탐색해야 하는 등 전체 맥락을 파악하기 어려웠다.
1차 해결: 도메인 단위로 바꿔보자
그래서 #15 PR에서 도메인 단위 구조로 전면 리팩토링했다.
변경 이유:
- 강의 관련 모든 것을 domains/lecture/에
- 리뷰 관련 모든 것을 domains/review/에
- 비즈니스 기능별 응집도 향상
// 1차 개선 - 도메인 단위 구조
domains/
├── lecture/
│ ├── components/ # 강의 관련 모든 컴포넌트
│ │ ├── LectureCard.tsx
│ │ ├── LectureList.tsx
│ │ └── LectureInfo.tsx
│ ├── skeleton/ # 강의 관련 스켈레톤
│ └── styles/ # 강의 관련 스타일
└── review/
├── components/ # 리뷰 관련 모든 컴포넌트
├── skeleton/
└── styles/
개선된 점:
- "LectureCard 어디 있지?" → domains/lecture/에서 바로 찾음
- 강의 기능 개발할 때 한 폴더에서 모든 작업 가능
- 팀 협업 시 도메인별 작업 분담 용이
그런데 새로운 고민이 생기기 시작했다
도메인 단위 구조는 분명 좋았다. 하지만 Next.js App Router 환경에서 개발하면서 아쉬운 점을 발견하기 시작했다.
Next.js 공식 문서에서 강조하는 내용:
"
use clientis used to declare a boundary between the Server and Client module graphs (trees)."
즉, 서버와 클라이언트 간의 경계를 명확하게 구분해야 한다는 것이다. 서버에서 렌더링될 컴포넌트와 클라이언트에서 동작할 컴포넌트를 의도적으로 분리해서 관리해야 성능 최적화와 명확한 아키텍처를 얻을 수 있다.
하지만 현재 구조의 DX 문제:
// domains/lecture/components/LectureCard.tsx
// 파일명만으로는 어떤 환경에서 실행되는지 알 수 없음
export default function LectureCard({ lecture }) {
// 'use client'가 있는지 파일을 열어봐야 알 수 있음
return <div>{lecture.name}</div>;
}개발하면서 느낀 불편함들:
- 개발 시 혼란: 컴포넌트를 수정하기 전에 먼저 파일을 열어서 서버/클라이언트 여부를 확인해야 함
- 번들 분석 어려움: 어떤 컴포넌트들이 클라이언트 번들에 포함되는지 한눈에 파악하기 어려움
- 팀 협업 문제: 새로운 팀원이 컴포넌트의 실행 환경을 즉시 파악하기 어려움
고민하다가 발견한 아키텍처 관련 글
구조에 대해 고민하던 중, 아래와 같은 조언을 접했다.
"Organize your code by the products and features that your company actually makes!"
출처: The Life-changing (And Time-saving!) Magic Of Feature Focused Code Organization!
즉, 기술적 역할(common, ui 등)별로 나누기보다는 실제 비즈니스 기능(lecture, review) 단위로 코드를 묶는 것이 더 효과적이라는 의미다.
실제로 우리도 이 원칙에 따라 도메인 중심 구조로 리팩토링을 진행했다.
하지만 Next.js는 또 한편으로 서버/클라이언트라는 기술적 분리를 요구하고 있었다.
딜레마: 둘 다 가져갈 수는 없을까?
도메인 단위 구조의 장점은 분명했다. 파일 찾기도 쉽고, 관련 기능끼리 모여있으니 개발할 때도 편했다.
그런데 Next.js 최적화도 포기하기 아쉬웠다.
선택지들:
-
옵션 1: components/server/, components/client/로 다시 바꾸기
- 그럼 LectureCard랑 ReviewCard가 또 흩어진다
-
옵션 2: 그냥 domains/ 구조 그대로 두기
- Next.js 최적화를 포기하기엔 아쉽다
그런데 둘 다 쓸 수는 없을까?
도메인 단위 구조는 유지하면서 서버/클라이언트도 구분할 방법이 없을까 고민하다가...
"domains/lecture/server/, domains/lecture/client/ 이렇게 하면 되지 않나?"
시도해보기: 둘 다 적용해보자
그래서 실험해보기로 했다.
바깥쪽은 도메인별로 나누고, 안쪽에서 서버/클라이언트를 구분하는 구조로.
// 도메인 단위 아키텍처
domains/
├── lecture/
│ ├── server/components/ # 서버 컴포넌트 (LectureCardHybrid 등)
│ ├── client/
│ │ ├── components/ # 클라이언트 컴포넌트 (LectureSelector 등)
│ │ └── hooks/ # 클라이언트 훅들 (useLectureRating 등)
│ ├── shared/types/ # 타입 정의
│ ├── skeleton/ # 로딩 UI
│ └── styles/ # CSS 모듈
└── review/
├── server/... # lecture와 동일한 구조
├── client/...
├── shared/...
└── ...
이렇게 하면:
- 강의 관련 작업할 때는 domains/lecture/만 보면 됨
- 서버 컴포넌트인지 클라이언트 컴포넌트인지도 명확
- Next.js 번들 분석할 때도 추적 쉬움
보너스 고민: 진짜 전역 컴포넌트는 어디에?
그렇다면 Header, Modal 같은 전역 공통 컴포넌트들은 어디에 둘까?
- 옵션 1: domains/shared/
- 옵션 2: 루트의 shared/
고민하다가 루트에 두기로 했다. "전역 공통"이라는 의미가 더 직관적으로 느껴졌기 때문이다. 개발할 때도 "이거 어디 있지?" 했을 때 바로 떠오르는 위치인 것 같았다.
실제 적용해보기
도메인 단위 아키텍처를 실제로 적용해보았다.
// domains/lecture/server/components/LectureCardHybrid.tsx
// 명확히 서버 컴포넌트임을 알 수 있음
export default function LectureCardHybrid({ lecture }: { lecture: Lecture }) {
return (
<div className={styles.LectureSelector}>
<h3>{lecture.name}</h3>
<p>{lecture.professor}</p>
</div>
);
}// domains/lecture/client/components/LectureSelector.tsx
'use client';
// 명확히 클라이언트 컴포넌트임을 알 수 있음
export default function LectureSelector({ onSelect }: SelectorProps) {
return (
<div className={styles.LectureSelector}>
{/* 인터랙티브 로직 */}
</div>
);
}배럴 export도 도메인별로 정리했다:
// domains/lecture/index.ts
// Lecture Domain - Server Components
export { default as LectureCardHybrid } from './server/components/LectureCardHybrid';
export { default as LectureListHybrid } from './server/components/LectureListHybrid';
// Lecture Domain - Client Components
export { default as LectureSelector } from './client/components/LectureSelector';
export { default as DynamicRating } from './client/components/DynamicRating';
// Lecture Domain - Hooks
export { useLectureRating } from './client/hooks/useLectureRating';결과
실제로 적용해보니 예상했던 것보다 잘 동작했다.
적용 결과:
- 도메인 응집도 유지: "강의 관련은 domains/lecture/에"
- 렌더링 전략 명확: "서버 컴포넌트는 server/에, 클라이언트 컴포넌트는 client/에"
- 번들 분석 용이: 클라이언트 번들에 포함될 컴포넌트 명확
- 개발자 경험: 직관적인 파일 탐색
- 관련 기능 응집: hooks, skeleton, styles까지 도메인별로 관리
구조별 비교 요약
| 구분 | 기존 구조 | 1차 도메인 단위 구조 | 2차 도메인 단위 구조 |
|---|---|---|---|
| 폴더 구조 | components/common, ui | domains/lecture/components | domains/lecture/server, client, shared |
| 파일 찾기 | 여러 폴더 검색 필요 | 도메인별로 명확 | 도메인+렌더링 전략 명확 |
| 번들 분석 | 추적 어려움 | 추적 어려움 | 서버/클라이언트 구분 명확 |
| 개발 경험 | 관련 파일 분산 | 도메인 응집도 향상 | 도메인 응집 + 렌더링 전략 분리 + 완전한 기능 응집 |
그런데 문득 의문이 들었다
구조를 완성하고 나니 생각이 들었다. 이거 너무 복잡한 거 아닌가?
컴포넌트 15개 정도에 domains/lecture/server/components/lecture-card.tsx 같은 4단계 깊이 구조라니. 단순히 components/lecture/, components/review/ 정도로도 충분했을 것 같은데.
하지만 창업 프로젝트다 보니 확장성이 계속 신경 쓰였다. 지금은 5개지만 성공하면 50개가 될 수도 있고, 팀원이 늘어나면 명확한 구조가 더 중요해질 것 같았다.
무엇보다 이전 글에서 리라이팅을 하게 된 이유가 바로 아키텍처 문제였다. 구조적 문제로 인한 성능 이슈와 유지보수의 어려움을 직접 겪어봤기 때문에, 이번엔 미리 신경 쓰고 싶었다.
지금 당장은 과할 수 있지만, 또 다시 리라이팅하는 상황은 피하고 싶었다.
배운 것들
이번 실험을 통해 몇 가지 인사이트를 얻었다.
완벽한 구조는 없다
각 구조마다 트레이드오프가 존재한다. 중요한 건 현재 상황에서 가장 중요한 가치를 식별하고, 그에 맞는 절충점을 찾는 것이다.
계층별 분리로 둘 다 챙길 수 있다
처음엔 폴더 구조를 도메인별로 나눌지, 아니면 서버/클라이언트 컴포넌트 구분에 맞출지 둘 중 하나만 선택해야 한다고 생각했다. 하지만 계층을 하나 더 두니까 두 가지 관심사를 모두 만족시킬 수 있었다.
- 외부 레이어: 비즈니스 도메인별 구조 (domains/)
- 내부 레이어: 클라이언트/서버/공용 계층 분리 (client/server/shared)
직관적인 구조가 중요하다
아무리 이론적으로 좋은 구조라도, 실제 개발할 때 직관적이지 않으면 의미가 없다. "이 파일이 어디 있을까?"라는 질문에 바로 답할 수 있는 구조가 좋은 구조다.