Skip to content
Published on

프론트엔드 아키텍처 완전 가이드 — React Server Components·Signals·Islands·Meta Frameworks·Bundlers를 2025년 기준으로 한 번에 정리

Authors

프롤로그 — "프론트엔드는 왜 또 바뀌었나"

2018년: "React 쓰세요" 2021년: "Next.js 쓰세요" 2023년: "Server Components? Astro? Remix?" 2025년: "Signals? RSC? Islands? Turbopack vs Rspack vs Vite?"

프론트엔드는 5년마다 혁명이 반복된다. 2025년의 핵심 단어 6개:

  1. RSC (React Server Components) — 서버에서 렌더, 번들 0 KB 추가
  2. Signals — 세밀한 반응성, Virtual DOM 우회
  3. Islands — 대부분 정적, 필요한 부분만 hydration
  4. Meta Framework — Next, Remix, Nuxt, SvelteKit
  5. Edge Runtime — Vercel, Cloudflare에서 V8 isolate 실행
  6. 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서버 렌더 + HydrationNext Pages, Nuxt 2
SSG빌드 시 정적 생성Astro, Gatsby, Hugo
ISR정적 + 주기적 재생성Next, Astro
Streaming SSR + RSC서버 컴포넌트 + 스트리밍Next App Router

Core Web Vitals로 보는 차이

지표SPASSRSSGRSC 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특징
SolidJScreateSignal, createEffectJSX 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 Signalssignal(), 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 — IntersectionObserver
  • client:idlerequestIdleCallback
  • client: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 선호 + CloudflareRemix(React Router 7)
성능/번들 크기 우선 + 학습 의지 → SvelteKit
Vue 숙련 + 범용 배포 → Nuxt
콘텐츠 사이트 (블로그/문서)Astro (React/Vue/Svelte 모두 지원)

6부 — State Management 2025

상태의 4가지 종류

  1. Server State (서버에서 가져옴) — TanStack Query, SWR
  2. URL State (쿼리, path) — useSearchParams, nuqs
  3. Form State — React Hook Form, TanStack Form
  4. 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 5esbuild + Rollup개발 매우 빠름, 생태계 대세SvelteKit, Nuxt, Remix
TurbopackRustNext.js 통합, dev 모드 안정화Next.js 15+
RspackRustWebpack 호환 APIModern.js, ByteDance 사내
esbuildGo10x 빠름, 간단라이브러리, 툴링
RolldownRustRollup 대체 (2025 rc)Vite의 미래
BunZig올인원 (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대 방식

  1. Utility CSS (Tailwind) — 2025 지배적
  2. CSS-in-JS (Emotion, Styled Components) — 쇠퇴
  3. 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

레이어별 도구

레이어도구비율
UnitVitest (Jest 대체), Bun test60%
ComponentTesting Library + Vitest, Storybook Test20%
E2EPlaywright (Cypress 대체 중)15%
VisualChromatic, Percy, Playwright5%

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 맞추기

  1. 이미지 최적화: WebP/AVIF, <Image> 컴포넌트, lazy loading
  2. 폰트: font-display: swap, <link rel="preload">
  3. CSS 최적화: Critical CSS inline, 나머지 defer
  4. JS 분할: import() dynamic, route-based splitting
  5. Prefetch: <Link prefetch>, rel="prefetch"
  6. CDN: 정적 자산 CDN, Edge Functions
  7. Compression: Brotli (20% 작음 vs gzip)
  8. React Compiler: 자동 memoization
  9. Server Components: 번들 감소
  10. Streaming SSR: TTFB 단축
  11. Database: N+1 해결, 인덱스
  12. 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가지

  1. useEffect로 데이터 fetching → TanStack Query 써라
  2. 모든 컴포넌트 Client Component → RSC 활용 못함
  3. Redux 보일러플레이트 폭발 → Zustand/Jotai로 이동
  4. CSS-in-JS runtime → 성능 저하, RSC 비호환
  5. Any prop drilling → Context or 상태관리
  6. 이미지 최적화 안 함 → LCP 박살
  7. Bundle analyzer 안 씀 → 무엇이 큰지 모름
  8. 접근성 무시 → 법적 리스크 (유럽, 미국)
  9. Lighthouse 안 봄 → 사용자 경험 나쁨
  10. 폼을 직접 useState로 → React Hook Form 써라

마무리 — "프론트엔드는 계속 변하지만 원칙은 같다"

2025년 프론트엔드의 3대 원칙:

  1. 서버에서 할 수 있는 건 서버에서 (RSC, Streaming)
  2. 보낼 JS는 최소로 (Islands, Signals, Code splitting)
  3. 사용자 경험 우선 (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 통합 전략

테스트 없으면 리팩터 없다. 다음 글에서.