✍️ 필사 모드: 프론트엔드 아키텍처 완전 가이드 — React Server Components·Signals·Islands·Meta Frameworks·Bundlers를 2025년 기준으로 한 번에 정리
한국어프롤로그 — "프론트엔드는 왜 또 바뀌었나"
2018년: "React 쓰세요" 2021년: "Next.js 쓰세요" 2023년: "Server Components? Astro? Remix?" 2025년: "Signals? RSC? Islands? Turbopack vs Rspack vs Vite?"
프론트엔드는 5년마다 혁명이 반복된다. 2025년의 핵심 단어 6개:
- RSC (React Server Components) — 서버에서 렌더, 번들 0 KB 추가
- Signals — 세밀한 반응성, Virtual DOM 우회
- Islands — 대부분 정적, 필요한 부분만 hydration
- Meta Framework — Next, Remix, Nuxt, SvelteKit
- Edge Runtime — Vercel, Cloudflare에서 V8 isolate 실행
- Rust-based Bundler — Turbopack(Vercel), Rspack(바이트댄스), Biome
클라우드 네이티브(Ep 15)가 인프라를 만들었다면, 프론트엔드는 그 위에서 사용자를 만난다.
이 글은 Season 2 Ep 16 — 프론트엔드 아키텍처. RSC가 왜 혁명인지, Signals가 왜 Virtual DOM을 우회하는지, Astro Islands의 실전, Meta Framework 4대 비교, State Management 2025 판, Rust Bundler 전쟁까지.
1부 — 프론트엔드 렌더링 모델 6가지
6가지 모델
| 모델 | 설명 | 대표 |
|---|---|---|
| SPA | 클라이언트 렌더링 | CRA, Vite + React |
| MPA | 서버 페이지 반환 | PHP, Rails |
| SSR | 서버 렌더 + Hydration | Next Pages, Nuxt 2 |
| SSG | 빌드 시 정적 생성 | Astro, Gatsby, Hugo |
| ISR | 정적 + 주기적 재생성 | Next, Astro |
| Streaming SSR + RSC | 서버 컴포넌트 + 스트리밍 | Next App Router |
Core Web Vitals로 보는 차이
| 지표 | SPA | SSR | SSG | RSC Streaming |
|---|---|---|---|---|
| FCP (First Contentful Paint) | 느림 | 보통 | 빠름 | 매우 빠름 |
| LCP | 느림 | 보통 | 빠름 | 빠름 |
| TTI (Time to Interactive) | 느림 | 보통 | 빠름 | 빠름 |
| TBT (Total Blocking Time) | 높음 | 보통 | 낮음 | 낮음 |
| SEO | 나쁨 | 좋음 | 최고 | 좋음 |
| 번들 크기 | 크다 | 크다 | 작다 | 최소 |
2025 표준: 콘텐츠 사이트는 Astro/Next RSC, 앱은 Next App Router / Remix.
2부 — React Server Components 이해하기
RSC는 무엇인가
전통 React: 모든 컴포넌트가 클라이언트 번들에 포함
RSC: 일부 컴포넌트를 "서버 전용"으로 표시
→ 서버에서 렌더, 결과만 전송
→ 클라이언트 번들에 포함 X
효과:
- 번들 크기 30-50% 감소
- DB/파일 접근 컴포넌트 가능
- 큰 라이브러리(marked, shiki 등) 서버만 사용
Server Component vs Client Component
// app/blog/page.tsx — 기본 Server Component
import { db } from '@/lib/db';
export default async function BlogPage() {
const posts = await db.post.findMany(); // 서버에서 직접 DB 접근
return (
<div>
{posts.map(p => <Article key={p.id} post={p} />)}
<Comments postId={1} />
</div>
);
}
// app/Article.tsx — 여전히 Server Component
export function Article({ post }) {
return <article>{post.content}</article>;
}
// app/Comments.tsx — Client Component ('use client' 지시어)
'use client';
import { useState } from 'react';
export function Comments({ postId }) {
const [text, setText] = useState('');
return <input value={text} onChange={e => setText(e.target.value)} />;
}
Server Component 규칙
할 수 있는 것:
- async/await (최상위)
- DB/파일시스템 접근
- 서버 환경 변수
- Node 라이브러리
할 수 없는 것:
useState,useEffect, 이벤트 핸들러- 브라우저 API (window, document)
- Context 소비는 가능하지만 Provider는 Client에서
"Pass-through" 패턴
// Server Component
import ServerStuff from './ServerStuff';
import ClientLayout from './ClientLayout';
export default function Page() {
return (
<ClientLayout>
<ServerStuff /> {/* Server Component를 Client Component의 children으로 */}
</ClientLayout>
);
}
핵심: Client Component가 Server Component를 import 할 수 없지만, children으로 받을 수 있음.
Streaming SSR + Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header /> {/* 즉시 렌더 */}
<Suspense fallback={<Skeleton />}>
<SlowArticle /> {/* 비동기 완료 시 스트리밍 */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<SlowComments />
</Suspense>
</div>
);
}
효과: 느린 부분이 빠른 부분을 막지 않음. 사용자는 LCP에 해당하는 콘텐츠 먼저 봄.
RSC의 한계 2025
- 생태계 미성숙: 많은 라이브러리가
use client를 요구 - Next App Router 한정: Remix는 Loader 모델, 다른 방식
- 디버깅 어려움: 서버/클라이언트 경계 헷갈림
- Monorepo/패키지 호환성: UI 라이브러리 author 고생
3부 — Signals — Virtual DOM의 대안
Virtual DOM의 비용
React 렌더링:
1. setState → 컴포넌트 함수 재실행
2. 새 VDOM 트리 생성
3. 이전 트리와 diff
4. 변경된 DOM 노드만 업데이트
문제: 컴포넌트 전체가 재실행됨 → 큰 컴포넌트에서 낭비
Signal 원리
// SolidJS
import { createSignal, createEffect } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
createEffect(() => console.log('count =', count()));
return (
<div>
<span>{count()}</span> {/* 이 부분만 업데이트 */}
<button onClick={() => setCount(count() + 1)}>+</button>
</div>
);
}
핵심: count()를 읽는 정확한 DOM 노드만 업데이트. 컴포넌트 재실행 X.
Signal 프레임워크 비교
| 프레임워크 | Signal API | 특징 |
|---|---|---|
| SolidJS | createSignal, createEffect | JSX but no VDOM, 2020년부터 |
| Svelte 5 Runes | $state, $derived, $effect | 컴파일러 최적화, 2024 |
| Angular 18+ | signal(), computed(), effect() | Google 공식 채택, 2024 |
| Vue 3 (Refs) | ref(), computed() | 오래된 Signal의 선배 |
| Preact Signals | signal(), computed() | React와 호환 가능 |
React의 대응: use()와 자체 상태
React는 Signal을 직접 도입하지 않고 **RSC + React Compiler(Forget)**로 대응.
- React Compiler: 컴포넌트 자동 memoization
- Activity API (실험): 보이지 않는 컴포넌트 상태 유지
언제 Signal을 고를까
적합:
- 빈번한 상태 업데이트 (게임, 실시간 대시보드)
- 성능 중요한 UI
- 팀이 새 패러다임 학습 가능
부적합:
- 기존 React 생태계 활용
- 대규모 팀 (학습 곡선)
4부 — Islands Architecture
개념
전통 SPA: 전체 페이지가 JS 앱 → 큰 번들, hydration 오래
Islands: 정적 HTML + 필요한 부분만 "섬"처럼 hydration
장점: 적은 JS, 빠른 LCP/TTI 단점: 섬 간 상태 공유 복잡
Astro 예시
---
// astro.build/page.astro (서버 실행)
import Counter from './Counter.tsx';
import { db } from '../lib/db';
const posts = await db.post.findMany();
---
<html>
<head><title>Blog</title></head>
<body>
<h1>Latest Posts</h1>
{posts.map(p => <article>{p.title}</article>)}
<Counter client:load /> <!-- 페이지 로드 시 hydration -->
<Comments client:visible /> <!-- 화면에 보일 때 hydration -->
<Chart client:idle /> <!-- 브라우저 유휴 시 -->
</body>
</html>
directive:
client:load— 즉시client:visible— IntersectionObserverclient:idle—requestIdleCallbackclient:media="(max-width: 500px)"— 미디어 쿼리- (지시어 없음) — 완전 정적
Fresh (Deno), Qwik, Marko
- Fresh: Deno의 Astro 아날로그, Preact 기반
- Qwik: "Resumable" — hydration 없이 이벤트 시점에 JS 로드
- Marko: eBay 제작, Islands 전 조상 (2014)
Islands 2025 현실
Astro: 블로그, 문서, 마케팅 사이트 지배 Qwik: 실험적이지만 흥미로운 아이디어(Resumability) Fresh: Deno 생태계에서 유지
5부 — Meta Framework 4대 비교 2025
Next.js 15 — 가장 큰 생태계
강점:
- App Router + RSC + Streaming
- Vercel 배포 1-click
- Image, Font, Analytics 내장
- Turbopack (Rust bundler) 개발 모드
- Partial Prerendering (실험) — 정적 shell + 동적 hole
약점:
- 복잡성 (App Router 학습 곡선)
- Vercel 락인 (일부 기능)
- 버전마다 breaking change
Remix (React Router 7로 병합, 2024)
강점:
- Web standards 중심 (Request/Response, FormData)
- Nested routing + Loaders/Actions
- Progressive Enhancement 철학
- React Router 팀 운영 → 라우팅 강력
- 어디든 배포 (Cloudflare, Node, Deno)
약점:
- RSC 지원 늦음 (2025 초)
- 생태계 Next 대비 작음
- Shopify 인수 후 React Router 7 통합
SvelteKit 2 — Svelte 5 Runes 시대
강점:
- Svelte 5 Runes → 최고 성능
- 번들 크기 작음
- SSR/SSG/SPA 모두 지원
- 학습 곡선 낮음
약점:
- 생태계 작음
- 대기업 채택 적음
- Runes 전환으로 혼란 (2024)
Nuxt 3 — Vue 생태계의 Next
강점:
- Vue 3 + Composition API
- Nitro (범용 서버, 여러 런타임 지원)
- 자동 import, auto-route
- 좋은 DX
약점:
- React 대비 생태계 작음
- 국제적 채택 Next 대비 낮음
선택 가이드 2025
팀이 React 숙련 + Vercel 인프라 → Next
Web standards 선호 + Cloudflare → Remix(React Router 7)
성능/번들 크기 우선 + 학습 의지 → SvelteKit
Vue 숙련 + 범용 배포 → Nuxt
콘텐츠 사이트 (블로그/문서) → Astro (React/Vue/Svelte 모두 지원)
6부 — State Management 2025
상태의 4가지 종류
- Server State (서버에서 가져옴) — TanStack Query, SWR
- URL State (쿼리, path) — useSearchParams, nuqs
- Form State — React Hook Form, TanStack Form
- Client State (클라이언트만) — Zustand, Jotai, Redux
TanStack Query — 서버 상태의 표준
function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5분
});
if (isLoading) return <Spinner />;
if (error) return <Error />;
return data.map(p => <Post key={p.id} />);
}
왜 필수: 캐싱, 중복 제거, 백그라운드 갱신, optimistic update, pagination, infinite scroll. 전부 기본 제공.
Zustand — Client 상태 승자
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
}));
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}
장점: Redux 대비 보일러플레이트 90% 적음, TypeScript 친화적, 4KB.
Jotai — Atomic State
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubledAtom);
return <div>{count} / {doubled}</div>;
}
특징: Recoil 영감, 작은 atom 조합, dependency tracking 자동.
Redux — 이제 선택? Toolkit으로 완전히 다름
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: (state) => { state.count += 1; }, // Immer로 mutation OK
},
});
const store = configureStore({ reducer: { counter: counterSlice.reducer } });
2025 현실: Redux는 대규모 엔터프라이즈에서 여전히 현역. 하지만 신규 프로젝트는 Zustand/Jotai 선호.
URL State — nuqs 2025
import { useQueryState } from 'nuqs';
function Search() {
const [query, setQuery] = useQueryState('q'); // ?q=...
return <input value={query ?? ''} onChange={e => setQuery(e.target.value)} />;
}
장점: URL이 Single Source of Truth. 뒤로가기, 공유 가능.
7부 — Form과 Validation 2025
React Hook Form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
age: z.number().min(18),
});
type FormData = z.infer<typeof schema>;
function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} />
<button type="submit">Submit</button>
</form>
);
}
2025 조합:
- React Hook Form (unmount 없이 빠름)
- Zod (스키마 + 타입)
- Server Action (Next) 또는 Mutation (TanStack Query)
TanStack Form — 새로운 플레이어 (2024)
headless, framework-agnostic. React Hook Form 대체 가능.
Server Action (Next) — Form의 미래
// actions.ts
'use server';
import { z } from 'zod';
const schema = z.object({ email: z.string().email() });
export async function subscribe(formData: FormData) {
const parsed = schema.safeParse({ email: formData.get('email') });
if (!parsed.success) return { error: parsed.error.message };
await db.subscriber.create({ data: parsed.data });
return { success: true };
}
// page.tsx
import { subscribe } from './actions';
export default function Page() {
return (
<form action={subscribe}>
<input name="email" />
<button>Subscribe</button>
</form>
);
}
장점: No API route, progressive enhancement, 타입 안전.
8부 — Bundler 전쟁 2025
역사
2015: Webpack 지배
2018: Parcel, Rollup 공존
2020: esbuild (Go, 10-100x 빠름)
2020: Vite (esbuild dev + Rollup prod)
2022: Turbopack (Vercel, Rust)
2023: Rspack (ByteDance, Rust, Webpack 호환)
2024: Rolldown (Rollup의 Rust 리라이트)
2025 주요 선수
| Bundler | 언어 | 특징 | 사용처 |
|---|---|---|---|
| Vite 5 | esbuild + Rollup | 개발 매우 빠름, 생태계 대세 | SvelteKit, Nuxt, Remix |
| Turbopack | Rust | Next.js 통합, dev 모드 안정화 | Next.js 15+ |
| Rspack | Rust | Webpack 호환 API | Modern.js, ByteDance 사내 |
| esbuild | Go | 10x 빠름, 간단 | 라이브러리, 툴링 |
| Rolldown | Rust | Rollup 대체 (2025 rc) | Vite의 미래 |
| Bun | Zig | 올인원 (bundler + runtime + pm) | 독립 사용 |
Vite가 왜 이겼나
1. Dev: esbuild로 ESM dev server → 즉시 업데이트
2. Build: Rollup으로 프로덕션 번들 (tree-shaking 최고)
3. Plugin 생태계: Rollup 플러그인 호환
4. Framework-agnostic: React, Vue, Svelte, Solid 모두
2025 체감: webpack 기반 프로젝트를 Vite로 옮기면 dev start 30초 → 1초.
Turbopack vs Rspack
Turbopack: Vercel이 Next.js용으로 최적화, 2025년 prod 모드 stable
Rspack: Webpack 플러그인/로더 그대로 → 마이그레이션 쉬움
선택: Next면 Turbopack, 기존 Webpack 프로젝트면 Rspack
9부 — CSS 전략 2025
3대 방식
- Utility CSS (Tailwind) — 2025 지배적
- CSS-in-JS (Emotion, Styled Components) — 쇠퇴
- CSS Modules — 꾸준, RSC 호환
Tailwind CSS v4 (2024)
<div class="flex items-center gap-4 rounded-lg bg-white p-6 shadow-lg dark:bg-gray-800">
<img class="h-12 w-12 rounded-full" src="..." />
<div class="text-gray-900 dark:text-white">
<h3 class="text-lg font-semibold">Title</h3>
<p class="text-sm text-gray-500">Description</p>
</div>
</div>
v4 변경: Rust 기반 엔진 (Oxide), 설정 CSS 파일로 이동, import 한 번.
CSS-in-JS의 몰락
2024-2025 변화:
- Emotion/Styled Components: RSC 비호환 → 사용률 감소
- Vanilla Extract: zero-runtime CSS-in-JS, RSC 호환
- Panda CSS: build-time 생성, Chakra 팀 신규 프로젝트
CSS Modules 재부상
// Button.module.css
.button { background: blue; padding: 0.5rem 1rem; }
// Button.tsx
import styles from './Button.module.css';
export function Button() {
return <button className={styles.button}>Click</button>;
}
장점: zero runtime, RSC 호환, 표준. Next/Remix 기본 지원.
shadcn/ui — 컴포넌트의 새 패턴
npx shadcn@latest add button card dialog
# → components/ui/ 에 코드가 복사됨 (npm install 아님)
철학: 패키지 X, 코드 복사. 자유롭게 수정. Radix UI + Tailwind. 2025 현실: React UI의 사실상 표준. Toolpad, Clerk, Vercel 모두 채택.
10부 — 프론트엔드 테스팅 2025
레이어별 도구
| 레이어 | 도구 | 비율 |
|---|---|---|
| Unit | Vitest (Jest 대체), Bun test | 60% |
| Component | Testing Library + Vitest, Storybook Test | 20% |
| E2E | Playwright (Cypress 대체 중) | 15% |
| Visual | Chromatic, Percy, Playwright | 5% |
Vitest — Jest 킬러
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders', () => {
const { getByRole } = render(<Button>Click</Button>);
expect(getByRole('button')).toHaveTextContent('Click');
});
});
왜 Jest 대체: Vite 기반 → 빠름, ESM 네이티브, TypeScript 지원 좋음.
Playwright — E2E의 승자
import { test, expect } from '@playwright/test';
test('user can sign up', async ({ page }) => {
await page.goto('/signup');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign up' }).click();
await expect(page.getByText('Welcome')).toBeVisible();
});
장점: Cypress 대비 빠름, 멀티 브라우저 (Chromium, Firefox, WebKit), parallel 기본.
11부 — 접근성(a11y) 2025
필수 체크리스트 10개
- 모든 이미지에 alt 텍스트
- 폼 label 연결 (
<label htmlFor="">) - 키보드 네비게이션 가능 (Tab, Enter, Escape)
- 포커스 가시성 (focus ring)
- ARIA roles 올바르게 (버튼은 button, 링크는 a)
- 색 대비 WCAG AA (4.5:1)
- 스크린 리더 테스트 (VoiceOver, NVDA)
- 모션 줄이기 존중 (
prefers-reduced-motion) - 언어 명시 (
<html lang="ko">) - 동적 콘텐츠 알림 (
aria-live)
도구
- axe DevTools (브라우저 확장)
- Lighthouse (Chrome DevTools)
- React Axe (개발 시 자동 감지)
- ESLint plugin jsx-a11y
Radix UI / shadcn/ui
접근성 기본 내장. 직접 구현 금지 — 라이브러리 활용.
12부 — 성능 최적화 12가지
Core Web Vitals 맞추기
- 이미지 최적화: WebP/AVIF,
<Image>컴포넌트, lazy loading - 폰트:
font-display: swap,<link rel="preload"> - CSS 최적화: Critical CSS inline, 나머지 defer
- JS 분할:
import()dynamic, route-based splitting - Prefetch:
<Link prefetch>, rel="prefetch" - CDN: 정적 자산 CDN, Edge Functions
- Compression: Brotli (20% 작음 vs gzip)
- React Compiler: 자동 memoization
- Server Components: 번들 감소
- Streaming SSR: TTFB 단축
- Database: N+1 해결, 인덱스
- Monitoring: Vercel Analytics, Web Vitals API
Vercel Speed Insights
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function Layout({ children }) {
return (
<html>
<body>
{children}
<SpeedInsights />
</body>
</html>
);
}
측정: LCP, FID, CLS, INP, TTFB — 실제 사용자 기준.
13부 — 6개월 로드맵
1개월차: Next.js 15 App Router 제대로 이해. RSC vs Client Component 경계 2개월차: Tailwind CSS v4 + shadcn/ui로 실전 UI. Radix 접근성 활용 3개월차: TanStack Query로 서버 상태, Zustand로 클라이언트 상태 4개월차: React Hook Form + Zod + Server Action. 폼 처리 패턴 5개월차: Playwright E2E, Vitest 단위 테스트. CI 통합 6개월차: Signals 학습 (SolidJS 소형 프로젝트). Islands (Astro) 시도
14부 — 체크리스트 12개
- Next App Router or Remix or Astro 선택 (프로젝트 성격)
- Server Components 활용 (번들 감소)
- TanStack Query로 서버 상태 관리
- React Hook Form + Zod로 폼 처리
- Tailwind CSS v4 + shadcn/ui
- Playwright E2E 테스트
- 이미지는 Next Image / Astro Image
- Lighthouse 90+ 점수 유지
- a11y 체크 (axe DevTools)
- Core Web Vitals 모니터링
- Vite / Turbopack / Rspack으로 빠른 dev
- CDN/Edge 배포 (Vercel, Cloudflare)
15부 — 안티패턴 10가지
- useEffect로 데이터 fetching → TanStack Query 써라
- 모든 컴포넌트 Client Component → RSC 활용 못함
- Redux 보일러플레이트 폭발 → Zustand/Jotai로 이동
- CSS-in-JS runtime → 성능 저하, RSC 비호환
- Any prop drilling → Context or 상태관리
- 이미지 최적화 안 함 → LCP 박살
- Bundle analyzer 안 씀 → 무엇이 큰지 모름
- 접근성 무시 → 법적 리스크 (유럽, 미국)
- Lighthouse 안 봄 → 사용자 경험 나쁨
- 폼을 직접 useState로 → React Hook Form 써라
마무리 — "프론트엔드는 계속 변하지만 원칙은 같다"
2025년 프론트엔드의 3대 원칙:
- 서버에서 할 수 있는 건 서버에서 (RSC, Streaming)
- 보낼 JS는 최소로 (Islands, Signals, Code splitting)
- 사용자 경험 우선 (Core Web Vitals, a11y)
프레임워크는 바뀌지만:
- HTML/CSS/JS는 여전히 기반
- 접근성은 영원히 중요
- 성능은 비즈니스에 직결
- 테스트 없으면 리팩터 못함
다음 글은 Season 2 Ep 17 — 테스팅 완전 가이드. Unit, Integration, E2E, Property-based, Contract Testing, Mutation Testing, Test Doubles, TDD vs BDD, CI 통합까지. "코드를 신뢰하려면 테스트가 있어야 한다."
다음 글 예고 — "테스팅 완전 가이드: Unit·Integration·E2E·Property·Contract·Mutation"
Season 2 Ep 17은:
- Testing Pyramid vs Trophy
- Test Doubles (Stub/Mock/Fake/Spy)
- Property-based Testing (fast-check, Hypothesis)
- Contract Testing (Pact)
- Mutation Testing (Stryker)
- Snapshot Testing 현명하게
- TDD vs BDD vs TLD
- CI 통합 전략
테스트 없으면 리팩터 없다. 다음 글에서.
현재 단락 (1/446)
2018년: "React 쓰세요"