Backgrounds
최근 Next.js 프로젝트에 Feature-Sliced Design(FSD) 패턴을 도입하게 되었습니다. FSD는 프론트엔드 프로젝트에서 모듈들의 레이어(layer)를 명확히 구분하고, 상위 레이어와 하위 레이어 간의 의존성 방향을 명확히 하여 모듈 간 결합도를 낮추고 변경 사항이 어디까지 영향을 미칠지 예측 가능하게 만들어줍니다. 이를 통해 유지 보수성이 크게 향상된다는 장점이 있습니다.
(자세한 설명은 이전 글을 참고해주세요)
하지만 Next.js와 FSD 패턴을 함께 사용하다 보니, 일부 컨벤션에서 충돌이 발생하게 되었고, 그 과정에서 몇 가지 문제점과 해결책을 찾아야 했습니다. 이번 글에서는 제가 겪었던 문제들과 이를 해결한 방법을 공유하려 합니다.
1. app 폴더 네이밍 충돌
Problems
Next.js의 App Router는 기본적으로 app 폴더를 경로(route)로 사용합니다. app 폴더 하위의 디렉토리는 라우트 경로가 되고, page.tsx 파일은 해당 경로에 접속했을 때 실행되는 페이지로 동작하죠.
반면 FSD 패턴에서 app 레이어는 애플리케이션 전반에 걸쳐 사용되는 provider, 라우트 정의, 전역 스타일 등을 포함하는 레이어로서, 애플리케이션의 실행과 관련된 모든 것이 포함됩니다. 이로 인해 Next.js의 라우팅 폴더와 FSD 패턴에서의 app 레이어 폴더가 같은 이름을 가지며 충돌하는 상황이 발생했습니다.
Solutions
FSD 공식 문서에서는 이런 충돌을 피하기 위해 root 폴더에 Next.js의 app 폴더를 두고, FSD 패턴의 app 폴더는 src 디렉토리 내에 두는 것을 권장하고 있습니다. 이를 그대로 적용해, Next.js의 app 폴더는 프로젝트 루트에, FSD의 app 레이어는 src/app 폴더 내에 배치하여 두 시스템이 서로 충돌하지 않도록 했습니다.
├── app # Next.js app directory
└── src
└── app # FSD app layer
2. middleware 인식 오류
Problems
Next.js에서 Middleware는 요청과 응답 사이에 실행되는 로직을 정의할 수 있는 중요한 파일입니다. 기본적으로 src 폴더 내 app, page 폴더 내에 middleware.ts(. js) 파일을 작성하면 Next.js에서 이를 인식하여 미들웨어로 동작합니다. 하지만 FSD 패턴을 적용해 app 폴더가 루트 하위에 위치하게 되면서, src/app/middleware로 작성할 경우 Next.js가 이를 미들웨어 파일로 인식하지 않는 문제가 발생했습니다.
Solutions
FSD 패턴을 사용하기 위해 app 폴더를 루트 하위에 위치시킨 경우, 미들웨어 파일 역시 루트 디렉토리에 직접 위치해야만 정상적으로 인식됩니다. 따라서 미들웨어 파일을 src/app/middleware가 아닌, 루트 디렉토리에 바로 작성함으로써 문제를 해결할 수 있었습니다.
├── app
├── middleware.ts # 루트 디렉토리에서 바로 작성
└── src
└── app
이를 통해 Next.js는 middleware.ts 파일을 정상적으로 인식하고 서버 미들웨어로 동작할 수 있었습니다.
3. Pages 레이어 네이밍
Problems
Next.js에서 App Router를 사용하더라도, src/pages 내에 파일이 존재할 경우 여전히 Pages Router가 지원됩니다.
그러나 FSD 패턴에서는 pages라는 폴더를 애플리케이션의 전체 페이지 또는 주요 페이지 컴포넌트를 모아둔 레이어로 사용합니다. 이로 인해 Next.js와 FSD 모두에서 pages라는 이름을 사용하게 되어 혼란이 생기고, Next.js가 src/pages 폴더를 라우트 경로로 인식해 버리는 문제가 발생했습니다.
Solutions
이 문제를 해결하기 위해 FSD에서 사용하는 pages 레이어의 이름을 views로 변경하였습니다. 이렇게 함으로써 Next.js와 FSD 패턴 간의 네이밍 충돌을 해결했고, 프로젝트 내 README 파일에 변경된 네이밍 컨벤션을 명시해 팀원들이 이를 따르도록 했습니다.
4. Public API 적용 시 코드 실행 환경 오류
Problems
FSD 패턴에서는 public API 라는 규칙을 적용(barrel export와 동일), 각 slice와 segment에서 외부에 노출하고자 하는 모듈들을 명시하고 그 이외의 기능들은 외부로부터 격리합니다. 이를 통해 각 layer 간 불필요한 의존성을 낮추고, 변경사항의 전파 범위를 제한할 수 있습니다.
하지만 이 경우 JS 모듈 시스템의 실행 방식으로 인해 import 한 파일 전체가 한 번씩 실행되면서, 서버/클라이언트 컴포넌트들의 실행 환경 불일치로 인한 오류가 발생했습니다.
예를 들어
// shared/utils/cookies
import { cookies } from "next/headers"; // 해당 모듈은 서버에서만 사용할 수 있습니다
export function getCookie(key: string) {
return cookies().get(key)?.value;
}
// shared/utils/some-utils
export function someUtils() {
}
위와 같이 server에서만 실행할 수 있는 getCookie함수와, client에서도 사용 가능한 someUtils 함수가 있을 때, public API 규칙을 지키기 위해서는
// shared/utils/index.ts
export * from './cookies'
export * from './some-utils'
이처럼 공개 진입점(index.ts) 파일을 만들어줘야만 합니다.
하지만 이때 client 컴포넌트에서 someUtils 함수를 사용하면
'use client'
import {someUtils} from '@/shared/utils'
const ClientComponent = () => {
const result = someUtils()
return <div>{result}</div>
}
클라이언트 컴포넌트에서 직접 서버 구성 요소(여기서는 cookies 함수)를 사용하지 않았음에도 불구하고, JS 모듈 시스템의 동작 방식으로 인해 에러가 발생하게 됩니다.
Solutions
서버 구성 요소를 사용하는 파일들을 별도 slice로 구분해 해결할 수 있습니다.
├── src/
└── shared/
├── server-utils // 서버 구성 요소들을 포함한 유틸리티 함수
├── utils // 서버, 클라이언트에서 모두 사용 가능한 유틸리티 함수
서버 구성 요소를 포함하거나 서버에서만 실행되어야 하는 모듈("use server"가 선언된 파일)들을 별로 slice로 분리했고, 해당 slice들은 서버 컴포넌트에서만 호출하도록 설계해 줬습니다.
Conclusion
최종적으로 Next.js에 FSD 폴더 아키텍처를 적용한 구조는 아래와 같습니다.
├── app/ # Next.js App Router 폴더 (루트 디렉토리)
│ ├── layout.tsx
│ └── page.tsx
├── src/
│ ├── app/ # FSD 패턴의 app 디렉토리
│ ├── views/ # FSD 패턴의 pages 디렉토리를 views로 변경
│ ├── widgets/
│ ├── features/
│ ├── entities/
│ └── shared/
├── middleware.ts # Next.js 미들웨어
├── package.json
├── tsconfig.json
└── ...
'source-code > FrontEnd' 카테고리의 다른 글
지속 가능한 프론트엔드 단위 테스트 작성법 (2) | 2024.10.09 |
---|---|
FrontEnd 개발자가 가장 빠르게 서비스를 구축하는 방법 (5) | 2024.09.08 |
[chrome extension] 이미지 파일 안전하게 불러오기 (0) | 2024.08.09 |
Feature-Sliced Design(FSD) 도입기 (0) | 2024.08.03 |
[chrome extension] dynamic import시 Cannot find module 에러 해결하기 (0) | 2024.07.26 |