불러오는 중...
불러오는 중...
sapan.dev is built server-component-first across all 16 locales. Notes from designing it that way: the bundle savings, the patterns I keep reaching for, and the moments I had to pull back from "everything server" because the UX needed it.
When I rebuilt sapan.dev on Next.js App Router, I went server-component-first by default — every component is a Server Component unless it has a real reason to be a Client Component. After several months in production across 16 locales, the bundle is significantly smaller than the previous Pages Router version, and the patterns have settled into something I would now reach for on most new React projects. Below is what I have learned, including the spots where I had to pull back from 'everything server' because the UX needed local state.
React Server Components (RSC) represent one of the most significant architectural shifts in React since hooks. With Next.js 15 making them the default, understanding how to use them effectively is no longer optional — it is essential.
Server Components are React components that run exclusively on the server. They can directly access databases, file systems, and internal services without any client-side JavaScript. Unlike traditional SSR, they do not hydrate on the client — what the server renders is what the browser receives as static HTML.
정보
Key insight: Server Components reduce your JavaScript bundle size dramatically. A component that imports a 200KB markdown parser only ships that parser to the server, not the browser.
The distinction between Server and Client components comes down to interactivity and data access. Server Components excel at data fetching and static rendering, while Client Components handle user interaction, browser APIs, and stateful logic.
// Server Component (default in Next.js 15)
async function BlogPost({ slug }: { slug: string }) {
// Direct database access — no API route needed
const post = await db.posts.findOne({ slug });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// Client Component — use only when needed
'use client';
import { useState } from 'react';
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? 'Liked!' : 'Like'}
</button>
);
}The most powerful pattern is passing Server Components as children to Client Components. This keeps your interactive wrappers lean while allowing rich server-rendered content inside them.
// ✅ Correct: Pass server component as children
function Page() {
return (
<InteractiveWrapper>
<ServerRenderedContent /> {/* runs on server */}
</InteractiveWrapper>
);
}
// ❌ Incorrect: Import server component inside client component
'use client';
import ServerRenderedContent from './ServerRenderedContent'; // breaks!Server Components enable a return to simplicity. Instead of managing loading states, error boundaries, and useEffect chains, you write async/await at the component level.
팁
Use generateStaticParams() for static pages and revalidate for ISR. This gives you the best of both worlds — static speed with fresh data.
Next.js 15 changed the default caching behavior significantly. fetch() calls are no longer cached by default — you must opt in explicitly. This makes caching behavior more predictable and easier to reason about.
// Static — cached indefinitely (SSG behavior)
fetch(url, { cache: 'force-cache' })
// Dynamic — no cache (SSR behavior)
fetch(url, { cache: 'no-store' })
// ISR — revalidate every 60 seconds
fetch(url, { next: { revalidate: 60 } })On sapan.dev, the server-first approach paid off in three concrete ways: the JS bundle is roughly half what the Pages Router version shipped, the locale-aware metadata generates entirely on the server (no flash of wrong language), and a lot of pages now ship with zero client JS at all. Where I pulled back: the contact modal, theme switcher, language switcher, and a handful of GSAP/Three.js scenes — all genuinely local-state interactive, all kept as Client Components without guilt. The mental model that worked best: design the tree as Server Components, and pinpoint Client islands where interactivity actually starts. RSC is not just a perf win — it is a different shape of React, and once you stop fighting it the simpler patterns return.
React의 다른 글
Notes from running the React Compiler on the BetterDocs admin (a 4-year-old codebase with ~80 components and useMemo scattered everywhere) and starting clean with it on xCloud v1. What broke, what did not, and where I still reach for manual memoization.
On the sapan.dev contact form I rewrote the React Hook Form + manual pending/error setup as a single useActionState call. The component dropped from ~80 lines to ~30, and useOptimistic gave the submit button a snappier feel. Notes on what Actions actually replace.
Templately runs on Redux Toolkit. TubeOnAI is React Query for server state and a small Zustand store for UI. sapan.dev gets by on Redux Toolkit + URL state. Three projects, three different state strategies, and the pattern that emerged for picking between them.