Skip to content

Next.js App Router와 SSR/CSR, layout을 바닐라 JS+Express로 구현하고, React diff·fiber·useState도 간단히 재현한 실습 프로젝트입니다.

Notifications You must be signed in to change notification settings

using2/mini-next

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

📄 Mini Next.js 프로젝트 기획서

🎯 프로젝트 개요

바닐라 JavaScript로 Next.js App Router의 핵심 메커니즘 구현

Next.js의 복잡한 최적화 레이어(RSC, Streaming, Turbopack)를 제외하고, App Router의 본질적인 동작 원리인 파일 기반 라우팅, SSR/CSR 전환, Layout 시스템, 데이터 페칭을 직접 구현하여 프레임워크의 설계 철학을 체득한다.


1. 프로젝트 배경

1.1 왜 이 주제를 선택했는가?

이전 경험: React의 VDOM, Diffing, Fiber 아키텍처를 바닐라 JavaScript로 직접 구현하며, "프레임워크를 재현하는 과정"이 깊은 이해로 이어진다는 것을 확인했다.

Next.js의 위치: 현대 웹 개발에서 Next.js는 단순한 프레임워크를 넘어 "웹 애플리케이션 아키텍처의 표준"이 되었다. 하지만 그 내부 동작은 여러 레이어로 추상화되어 있어 블랙박스처럼 느껴진다.

학습 목표: App Router의 핵심 메커니즘을 직접 구현함으로써:

  • 서버/클라이언트 경계를 어떻게 설계하는지
  • 파일 시스템이 어떻게 라우터가 되는지
  • SSR에서 CSR로 자연스럽게 전환되는 구조
  • Layout이 중첩되면서도 효율적으로 렌더링되는 방법

이 모든 것을 코드 레벨에서 이해하고자 한다.

1.2 도전 과제

도전 요소 상세 설명
서버/클라이언트 경계 설계 "use client" 지시어를 통해 컴포넌트 렌더링 위치를 결정하고, SSR과 CSR의 책임을 명확히 분리
파일 기반 라우팅 엔진 디렉토리 구조를 스캔하여 라우트 트리를 생성하고, 동적 세그먼트([id])를 파싱하는 매칭 알고리즘 구현
Layout 중첩 시스템 상위 layout이 하위 페이지를 감싸는 구조를 DFS로 수집하고, 효율적으로 조립
Hydration 전략 서버에서 생성된 HTML에 클라이언트 이벤트를 연결하는 "부분 Hydration" 구현

2. 구현 완료 기능

2.1 파일 기반 라우팅 시스템

구현 내용

  • app/ 디렉토리를 재귀적으로 스캔하여 라우트 트리 생성
  • 동적 라우트 [id] 지원 및 파라미터 추출
  • 부모 노드 참조를 통한 Layout 상속 구조

동작 방식

  • app/blog/[id]/page.jsx/blog/123 URL 매핑
  • 정적 라우트 우선 매칭, 동적 라우트는 fallback
  • params 객체로 동적 세그먼트 추출 ({ id: "123" })

2.2 SSR (Server-Side Rendering)

렌더링 파이프라인

요청 수신 
  → 라우트 매칭
  → getData() 호출 (있는 경우)
  → 컴포넌트 렌더링
  → Layout 중첩 적용
  → HTML 응답

Layout 중첩 로직

  • 부모 노드를 따라가며 모든 상위 layout 수집
  • 수집된 layout을 역순으로 적용하여 중첩 구조 생성
  • 최종적으로 <RootLayout><PageLayout><Page /></PageLayout></RootLayout> 형태

서버/클라이언트 분기

  • 파일 첫 줄에 "use client" 지시어가 있으면 클라이언트 컴포넌트로 분류
  • 클라이언트 컴포넌트는 서버에서 <div data-client="..."> placeholder만 렌더링
  • 브라우저에서 해당 placeholder를 찾아 실제 컴포넌트로 hydrate

2.3 CSR (Client-Side Rendering)

Hydration 시스템

  • 서버에서 생성된 data-client 속성을 가진 DOM 노드를 탐색
  • 해당 경로의 JavaScript 모듈을 동적 import
  • 기존 DOM을 유지하면서 이벤트 리스너만 연결 (부분 Hydration)
  • 각 클라이언트 컴포넌트는 독립적으로 hydrate

렌더링 최적화

  • Transform 캐싱: 파일 mtime + size 기반 해시로 Babel 변환 결과 캐시
  • Hot Reload: 5초마다 라우트 트리 재스캔 (개발 모드)
  • 부분 Hydration: data-client 속성이 있는 DOM만 활성화

2.4 데이터 페칭

페이지 API 인터페이스

// app/blog/[id]/page.jsx
export async function getData({ params }) {
  const post = await fetchPost(params.id);
  return { post };
}

export default function Page({ data }) {
  return <h1>{data.post.title}</h1>;
}

실행 흐름

// render.js
const Page = await loadComponent(node.page);
const data = Page.getData 
  ? await Page.getData({ params }) 
  : null;

element = createElement(Page.default, { data, params });

3. 핵심 설계 결정

3.1 아키텍처 레이어

┌─────────────────────────────────────┐
│         Application Layer            │
│    (app/*/page.jsx, layout.jsx)     │
└─────────────────────────────────────┘
           ↓
┌─────────────────────────────────────┐
│          Router Layer                │
│  (scan.js, matcher.js, load.js)     │
└─────────────────────────────────────┘
           ↓
┌──────────────────┬──────────────────┐
│   Server Layer   │   Client Layer   │
│  (render.js)     │  (hydrate.js)    │
└──────────────────┴──────────────────┘
           ↓
┌─────────────────────────────────────┐
│         Runtime Layer                │
│  (createElement, VDOM, Diff)         │
└─────────────────────────────────────┘

3.2 주요 기술 선택

영역 선택 이유
모듈 시스템 ESM Node.js에서 최신 표준이며, 동적 import 지원
JSX 변환 Babel 서버(CommonJS)와 클라이언트(ESM) 각각 변환
라우팅 파일 시스템 Next.js App Router 구조를 그대로 재현
상태 관리 useState(Fiber 기반) React Hooks API와 동일한 인터페이스

3.3 제외된 기능 (의도적)

  • React Server Components: 복잡한 직렬화 프로토콜 제외
  • Streaming SSR: 점진적 렌더링 대신 완전한 HTML 생성
  • Code Splitting: 번들링 시스템 없이 동적 import만 사용
  • Middleware: 라우팅 레벨에서만 처리

4. 프로젝트 구조

mini-next/
├── app/                    # 애플리케이션 코드
│   ├── layout.jsx          # 루트 Layout
│   ├── page.jsx            # 홈 페이지
│   ├── blog/
│   │   └── [id]/
│   │       └── page.jsx    # 동적 라우트
│   └── counter/
│       └── page.jsx        # 클라이언트 컴포넌트
│
├── core/                   # 프레임워크 코어
│   ├── router/
│   │   ├── scan.js         # 파일 시스템 스캔
│   │   ├── matcher.js      # URL 매칭 알고리즘
│   │   └── load.js         # 컴포넌트 로더
│   ├── ssr/
│   │   ├── render.js       # SSR 엔진
│   │   └── renderToString.js
│   ├── jsx/
│   │   └── createElement.js # JSX 런타임
│   └── client-hooks.js     # useState, render 등
│
├── client/                 # 클라이언트 런타임
│   ├── hydrate.js          # Hydration 로직
│   └── navigation.js       # SPA 네비게이션 (미구현)
│
└── server/
    └── server.js           # Express 서버 + 캐싱

5. 성능 최적화

5.1 개발 모드 최적화

Transform 캐싱 전략

  • 파일의 수정 시간(mtime)과 크기를 조합한 해시 생성
  • Babel transform 결과를 메모리 캐시에 저장
  • 파일이 변경되지 않으면 캐시에서 즉시 반환

측정 결과

첫 요청:     page.js → 31ms (transform)
두 번째 요청: page.js → 20ms (cache hit) ✅
개선율:      약 35.5% 감소

5.2 라우트 트리 자동 갱신

개발 모드에서 5초마다 app/ 디렉토리를 재스캔하여 새로운 페이지 추가나 삭제를 자동 감지합니다. 서버 재시작 없이 파일 시스템 변경사항이 반영됩니다.


6. 실행 결과 분석

6.1 네트워크 성능

컴포넌트 타입 초기 HTML Script 로딩 총 시간
Server Component 17-42ms - 17-42ms
Client Component 5ms 2-8ms 7-13ms

분석:

  • Server Component는 SSR 오버헤드가 있지만 즉시 콘텐츠 표시
  • Client Component는 초기 응답은 빠르지만 JS 다운로드 + 실행 필요
  • 실제 Next.js 프로덕션 빌드와 유사한 패턴

6.2 개선 여지

프로덕션 모드를 구현한다면, 빌드 타임에 모든 컴포넌트를 사전 변환하여 .next/ 디렉토리에 저장할 수 있습니다. 런타임에는 이미 변환된 코드를 바로 사용하여 SSR 시간을 17-42ms에서 5-10ms로 단축할 수 있습니다.


7. 배운 점과 인사이트

7.1 프레임워크 설계 철학

"파일 시스템 = 라우터"

  • Convention over Configuration의 실제 구현
  • 디렉토리 구조가 곧 URL 구조가 되는 것의 장점
  • 개발자가 라우팅 로직을 작성하지 않아도 됨

서버/클라이언트 경계

  • 단순한 문자열 지시어("use client")로 렌더링 위치 결정
  • 서버는 HTML을, 클라이언트는 interactivity를 담당
  • 명확한 책임 분리가 복잡도를 낮춤

Layout 중첩의 효율성

  • 상위 Layout은 변경되지 않고 재사용
  • 페이지 전환 시 필요한 부분만 업데이트
  • SPA의 장점(빠른 전환) + SSR의 장점(초기 로딩)

7.2 기술적 도전

Babel Transform의 서버/클라이언트 분기

// 서버: CommonJS (eval로 실행)
plugins: [
  ['@babel/plugin-transform-modules-commonjs']
]

// 클라이언트: ESM (브라우저 native import)
presets: [[
  '@babel/preset-env', 
  { modules: false }
]]

Layout 상속 구조

  • 단순 재귀로는 부모 참조 불가
  • parent 속성을 라우트 트리에 추가하여 해결
  • DFS로 상위 layout 수집 후 역순으로 감싸기

8. 향후 확장 계획

8.1 미구현 기능

기능 현재 상태 구현 방법
SPA Navigation 미구현 navigation.js에서 <a> 클릭 가로채기 + history.pushState
Metadata API 미구현 generateMetadata() 함수 지원
Error Boundary 미구현 error.jsx 파일 기반 에러 처리
Loading UI 미구현 loading.jsx + Suspense 구현

8.2 최적화 방향

프로덕션 빌드 시스템

  • 모든 페이지를 빌드 타임에 사전 변환
  • 변환된 결과를 .next/ 디렉토리에 저장
  • 런타임에는 사전 변환된 코드를 즉시 사용

Code Splitting

  • 현재는 모든 페이지가 독립적으로 로드
  • 페이지별 번들 생성 및 공통 의존성 분리
  • Dynamic import를 통한 lazy loading 최적화

9. 결론

9.1 프로젝트 성과

Next.js App Router의 핵심 메커니즘을 바닐라 JavaScript로 재현

  • 파일 기반 라우팅
  • SSR/CSR 전환
  • Layout 중첩
  • 데이터 페칭
  • 부분 Hydration

실제 동작하는 MVP 완성

  • 동적 라우트 지원
  • 서버/클라이언트 컴포넌트 분리
  • 개발 모드 최적화 (캐싱, Hot Reload)

프레임워크 설계 철학 체득

  • Convention over Configuration
  • 레이어 분리 아키텍처
  • 성능과 개발 경험의 트레이드오프

9.2 개인적 성장

이전 프로젝트(React 구현): "프레임워크가 무엇을 하는가"를 이해 이번 프로젝트(Next.js 구현): "프레임워크가 어떻게 설계되는가"를 이해

단순히 API를 사용하는 것을 넘어, 왜 그런 API가 필요한지, 내부에서 어떤 문제를 해결하고 있는지를 코드 레벨에서 경험했다.

9.3 향후 방향

이번 미션을 통해 구축한 기반 위에:

  1. SPA Navigation을 추가하여 완전한 풀스택 프레임워크로 발전
  2. 실제 프로젝트에 적용하며 실전 경험 축적
  3. 오픈소스로 공개하여 피드백 수렴

최종 목표: "프레임워크를 만드는 사람의 시각"으로 기술을 바라보는 개발자

About

Next.js App Router와 SSR/CSR, layout을 바닐라 JS+Express로 구현하고, React diff·fiber·useState도 간단히 재현한 실습 프로젝트입니다.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published