Skip to content

렌더링 전략과 인증 ​

HTML을 언제 어디서 만드는지에 따라 첫 화면 속도, 검색 노출, 서버 비용이 달라집니다. 선택 기준과 그 위에서 인증을 다루는 방법을 정리합니다.

CSR, SSR, SSG의 차이 ​

같은 화면을 만드는 세 가지 시점입니다.

HTML을 만드는 시점첫 응답에 담기는 것
CSR브라우저에서, 요청 후빈 <div id="root">와 스크립트
SSR서버에서, 요청마다완성된 HTML
SSG빌드할 때, 한 번미리 만들어 둔 HTML 파일

CSR은 스크립트를 받고 실행한 뒤에야 글자가 보입니다.

html
<body>
  <div id="root"></div>
  <script src="/assets/main.js"></script>
</body>

SSR은 서버가 같은 컴포넌트를 실행해 HTML로 만들어 보냅니다. 첫 화면이 바로 보이고, 데이터도 서버에서 가져오므로 요청 폭포수가 줄어듭니다. 대신 요청마다 서버가 일합니다.

SSG는 그 일을 배포 시점에 한 번만 합니다. 내용이 자주 바뀌지 않으면 가장 빠르고 가장 쌉니다.

선택 기준 ​

페이지 단위로 고릅니다. 한 서비스 안에서 섞어 쓰는 것이 보통입니다.

화면선택
문서, 블로그, 랜딩SSG
상품 목록·상세처럼 검색에 걸려야 하는 화면SSR 또는 ISR
로그인 후 대시보드, 관리자 화면CSR
실시간 편집기, 지도CSR

판단할 때 보는 것은 세 가지입니다.

  • 검색 노출이 필요한가 — 필요하면 HTML에 내용이 있어야 합니다
  • 내용이 사용자마다 다른가 — 다르면 미리 만들어 둘 수 없습니다
  • 얼마나 자주 바뀌는가 — 드물게 바뀌면 빌드 시점으로 밀 수 있습니다

내용이 가끔 바뀌는 중간 지대에는 ISR이 있습니다. 미리 만들어 두고 일정 시간이 지나면 백그라운드에서 다시 만듭니다.

js
export const revalidate = 60;   // 60초마다 갱신

메타 프레임워크 ​

렌더링 전략, 라우팅, 번들링, 배포를 한 묶음으로 제공합니다.

기반특징
Next.jsReact서버 컴포넌트, 페이지별 전략 선택
NuxtVue파일 기반 라우팅, 자동 임포트
SvelteKitSvelte어댑터로 배포 대상 교체
Astro여러 프레임워크기본은 HTML, 필요한 곳만 섬처럼 동작

Astro의 방식은 따로 볼 만합니다. 페이지 전체를 정적 HTML로 내고, 상호작용이 필요한 컴포넌트에만 JavaScript를 보냅니다.

astro
<Header />                    <!-- HTML만 나갑니다 -->
<SearchBox client:visible />  <!-- 화면에 보일 때 로드됩니다 -->

읽는 화면이 대부분인 사이트에서는 보내는 JavaScript가 크게 줄어듭니다.

하이드레이션 비용 ​

SSR이 보낸 HTML은 그림일 뿐입니다. 브라우저가 같은 컴포넌트를 한 번 더 실행해 이벤트를 붙여야 동작합니다. 이 과정이 하이드레이션입니다.

HTML 도착 → 보이지만 눌러도 반응 없음 → JS 실행 → 그제야 동작

이 틈에서 체감 문제가 생깁니다. 버튼이 보이는데 눌리지 않는 구간입니다. INP 지표가 나빠지는 흔한 원인이기도 합니다.

서버와 클라이언트의 결과가 다르면 경고와 함께 화면이 어긋납니다.

jsx
<p>{new Date().toLocaleString()}</p>    // 서버 시간과 브라우저 시간이 다릅니다
<p>{Math.random()}</p>                   // 매번 다릅니다

브라우저에서만 알 수 있는 값은 마운트 이후에 넣습니다.

jsx
const [now, setNow] = useState(null);
useEffect(() => setNow(new Date()), []);

부분 렌더링과 스트리밍 ​

전체 페이지를 기다리지 않고, 준비된 부분부터 내보낼 수 있습니다.

jsx
<Layout>
  <ProductInfo />                        {/* 바로 나갑니다 */}
  <Suspense fallback={<ReviewSkeleton />}>
    <Reviews />                          {/* 느린 쿼리는 나중에 */}
  </Suspense>
</Layout>

느린 영역 하나가 페이지 전체를 붙잡지 않게 됩니다. 서버 컴포넌트를 쓰면 그 컴포넌트의 코드 자체가 브라우저로 가지 않아 번들도 줄어듭니다.

다만 모든 것을 서버로 옮기면 작은 상호작용에도 왕복이 생깁니다. 입력 중 반응, 드래그, 애니메이션은 클라이언트에 남깁니다.

인증 전략: 세션과 토큰 ​

두 방식의 차이는 서버가 상태를 들고 있는가입니다.

세션토큰(JWT)
저장 위치서버에 세션, 브라우저에 식별자토큰 자체에 정보
무효화서버에서 지우면 끝만료 전까지 유효
확장공유 저장소 필요서버가 검증만 하면 됨

프론트엔드에서 가장 중요한 선택은 어디에 저장하는가입니다.

js
localStorage.setItem("token", token);   // XSS 한 번이면 그대로 털립니다

스크립트가 읽을 수 있는 곳에 두면 XSS의 가치가 올라갑니다. 서버가 HttpOnly 쿠키로 내려 주는 방식이 기본입니다.

Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/

JavaScript가 읽을 수 없고, 요청에 자동으로 실립니다. 자동으로 실리는 만큼 CSRF 대응이 함께 필요합니다.

js
await fetch("/api/todos", {
  method: "POST",
  credentials: "include",   // 교차 출처라면 명시해야 쿠키가 실립니다
});

액세스 토큰을 짧게 두고 리프레시 토큰으로 갱신한다면, 만료 응답을 한곳에서 처리합니다.

js
if (response.status === 401) {
  await refresh();           // 동시에 여러 요청이 401을 받을 수 있습니다
  return retry(request);     // 갱신은 한 번만 수행되게 묶습니다
}

소셜 로그인 연동 ​

브라우저에서 처리할 수 있는 흐름은 리디렉션까지입니다.

앱 → 제공자 로그인 화면 → 코드와 함께 앱으로 복귀 → 서버가 코드를 토큰으로 교환

코드를 토큰으로 바꾸는 요청에는 클라이언트 시크릿이 필요합니다. 브라우저 코드에 두면 누구나 볼 수 있으므로 반드시 서버에서 교환합니다.

js
// 프론트엔드가 하는 일은 여기까지입니다
location.href = `${authorizeUrl}?client_id=${id}&redirect_uri=${uri}&state=${state}`;

state는 돌아온 응답이 내가 보낸 요청에 대한 것인지 확인하는 값입니다. 생성해서 보관하고, 복귀 시 비교합니다. 빠뜨리면 CSRF가 됩니다.

보호된 라우트 ​

화면을 가리는 것은 사용자 경험이지 보안이 아닙니다.

jsx
function RequireAuth({ children }) {
  const { user, isPending } = useUser();

  if (isPending) return <Loading />;          // 깜빡임을 막습니다
  if (!user) return <Navigate to="/login" replace state={{ from: location }} />;
  return children;
}

세 가지를 같이 챙깁니다.

  • 로딩 상태 — 확인 전에 로그인 화면으로 보내면 새로고침마다 튕깁니다
  • 돌아갈 주소 — 로그인 후 원래 가려던 곳으로 보냅니다
  • 권한 분기 — 로그인 여부와 역할은 다른 문제입니다

그리고 서버에서 같은 검사를 반복합니다. 브라우저 코드는 전부 열어 볼 수 있고, API는 주소만 알면 직접 호출됩니다. 프론트엔드의 라우트 가드는 잘못 들어온 사용자를 안내하는 장치일 뿐입니다.