[NextJS] App router에서 Emotion을 권장하지 않는 이유와 React Streaming SSR을 곁들인

2025. 6. 16. 23:20·Next.js & React

개요

  • 인턴 업무를 하며 Next14+Page Router프로젝트를 Next15+App Router로 변경하는 일을 받았다.
  • 공식문서를 뒤져보고, 블로그를 찾아봐도 다들 Emotion이랑 App router를 쓰지 말라는 말뿐.. 왜 인지 알려주지 않았다. https://nextjs.org/docs/app/guides/css-in-js
  • 팀에서 App router를 적용하지 않아야 하는 명확한 근거가 필요했기에 근거를 찾기 위해 고군분투한 과정을 담아본다. 해당 정보가 틀릴 수 있다는 점에 유의하시길 바랍니다..!
  • 개인적으로는 Next측에서 Page Router에 대한 지원을 거의 하지 않기 때문에 사이드 프로젝트나 프로젝트 도입초기라면 Tailwind/Styled-Component/Vanilla Extract + Next15+App Router로 ASAP하게 갈아타길 바란다.

 

 

React Streaming SSR이 뭘까?

React 18부터 생겨난 개념으로 기존의 SSR을 보완한 개념이다. (https://github.com/reactwg/react-18/discussions/37 해당 글을 읽어보면 좋다!)

SSR의 순서를 생각해보면 이러하다.

1️⃣ 서버에서 앱 전체의 데이터를 가져온다
2️⃣ 서버에서 앱 전체를 HTML로 렌더링하고 응답으로 보낸다.
3️⃣ 클라이언트에서 앱 전체의 JavaScript 코드를 로드한다.
4️⃣ 클라이언트에서 서버가 생성한 HTML과 JS 로직을 연결(hydration)한다.

 

여기서 눈에 띄는건 전체라는 개념이다. SSR도 결국 Atomic한 프로세스를 가지고 있기에, 전체가 다 끝나지 않으면 다음 단계를 진행할 수 없다.

 

Suspense를 생각해보자!

하지만 React18부터 새로 도입된 개념이 있는데, 바로 <Suspense>이다! Suspense를 사용해 Skeleton화면을 보여주게 되면서, 사용자가 기다리는 시간을 시각적인 효과로 줄여준다. Suspense를 사용해, 준비된 부분은 보여주고 준비되지 않은 부분은 Skeleton 처리를 한다.

이런 기능을 가능하게 해주는것이 Streaming API이다!

 

Streaming HTML과 Selective Hydration

React 18부터는 SSR에 두가지 중요한 API를 제공한다.

1️⃣ 스트리밍 HTML
renderToPipeableStream API를 통해 데이터를 다 가져오기 전이라도 준비된 HTML부터 스트리밍

https://ko.react.dev/reference/react-dom/server/renderToPipeableStream

 

renderToPipeableStream – React

The library for web and native user interfaces

ko.react.dev

 

2️⃣ 선택적 Hydration (Selective Hydration)
hydrateRoot API와 <Suspense>를 활용해 JS 코드 일부만 로드되더라도 해당 부분부터 우선적으로 hydration

 

✅ Recap

React18부터 전체 HTML을 렌더링해서 Hydration하는 것이 아닌, 부분적으로 렌더링이 완료됐다면 부분적으로 완료된 애들만 Hydration시킨다.

 

NextJS의 Streaming

Next14버전부터 3가지 서버 컴포넌트 전략을 취하고 있다. (https://nextjs.org/docs/14/app/building-your-application/rendering/server-components)

 

Static Rendering, Dynamic Rendering, Streaming이다.

Server Components를 사용하면 렌더링 작업을 여러 조각으로 나눠 준비된 순서대로 클라이언트로 스트리밍할 수 있다. 전체 페이지가 끝날때까지 기다리지 않고 일부화면을 더 빨리 보여줄 수 있다. 이렇게 조각별로 화면이 보여진다면 조각 하나 하나에 Style이 적용되어야 FOUC (화면 깜빡임)가 없을것이다. 이제 이 메커니즘을 가지고 Emotion으로 가보자! 🚀

 

Emotion은  Server Side Rendering 시 어떻게 Style 주입을 할까?

Emotion의 ServerSide Rendering 방식은 두가지이다!

기본적 접근, Advanced 접근이 있다.

기본적 접근

import { renderToString } from 'react-dom/server'
import App from './App'

let html = renderToString(<App />)

 

Emotion 10 이상에서는 @emotion/react와 @emotion/styled만 사용하는 경우 추가 설정 없이 SSR이 바로 동작한다.
React의 renderToString 또는 renderToNodeStream 메서드를 직접 호출하면 된다.

엇 이러면 Emotion을 App router에서 쓰면 되지 않을까? 싶었는데

생각해보니 현재 프로젝트에서도 전부다 Server Component가 아닌 몇몇 페이지는 Client Component가 섞여있어야 했다.

App Router에서 Server Component와 Client Component를 섞어 Chunk 단위로 Streaming → 예상치 못한 style + element 순서 mismatch 가능해 보였다.

 

Advanced한 접근

import { CacheProvider } from '@emotion/react'
import { renderToString } from 'react-dom/server'
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'

const key = 'custom'
const cache = createCache({ key })
const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache)

const html = renderToString(
  <CacheProvider value={cache}>
    <App />
  </CacheProvider>
)

const chunks = extractCriticalToChunks(html)
const styles = constructStyleTagsFromChunks(chunks)

res
  .status(200)
  .header('Content-Type', 'text/html')
  .send(`<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>My site</title>
    ${styles}
</head>
<body>
    <div id="root">${html}</div>

    <script src="./bundle.js"></script>
</body>
</html>`);

nth-child나 유사한 선택자를 사용하지 않는다면 이 방식을 사용할 필요가 없다.

전체 HTML이 끝나야 style 추출이 되기 때문에 Streaming SSR과는 본질적으로 호환되지 않는다!

 

결론

결론은 Page Router + Next15 현상유지를 하기로 하였다.

1) 공식문서에서 App Router가 Emotion을 호환하지 않는 내가 모르는 이유가 분명히 있을 것이라 생각했고, React Life Cycle과 Context에 의존하는 Emotion이 서버 컴포넌트에서 제대로 작동한다는 보장도 없었다.

2) 내가 담당한 프로젝트가 랜딩 페이지이기 때문에, 후에 일어날 수 있는 리스크에 너무 많은 리소스를 쏟고 싶지 않았다. 

사실상 App Router를 도입하려는 것도 서버 자원을 최대한 사용하여 브라우저에서 해석할 컴포넌트가 줄이고, JavaScript 번들 파일의 크기가 줄어드는 효과를 얻기 위함이다. 하지만 랜딩 페이지 특성상 이정도로 번들 파일이 커지지 않을 것이기 때문이다.

3) SEO를 챙기는게 우선인데 Emotion챙기다가 Client Component로 만들어버리면 SEO도 못챙기게 생겨버렸기 때문이다..!

 

Appendix

 

https://www.joshwcomeau.com/react/css-in-rsc/

https://github.com/reactwg/react-18/discussions/37

https://www.promleeblog.com/blog/post/200-react-ssr-1

https://emotion.sh/docs/ssr

https://velog.io/@seeh_h/React-Server-Component-Streaming-SSR에-대한-고찰

https://nextjs.org/learn/dashboard-app/streaming

https://emotion.sh/docs/ssr#default-approach

https://nextjs.org/docs/14/app/building-your-application/rendering/server-components

https://saengmotmi.netlify.app/react/streaming_ssr/

'Next.js & React' 카테고리의 다른 글

[React] Controlled input과 Uncontrolled input은 혼용하면 안된다!  (1) 2025.04.29
[Next.js] 헤드리스 컴포넌트로 드롭다운 만들기!  (0) 2024.02.14
'Next.js & React' 카테고리의 다른 글
  • [React] Controlled input과 Uncontrolled input은 혼용하면 안된다!
  • [Next.js] 헤드리스 컴포넌트로 드롭다운 만들기!
jjungking
jjungking
쩡킹의 고상한 코딩 이야기
  • jjungking
    jjungking
    jjungking
  • 전체
    오늘
    어제
    • 분류 전체보기 (19)
      • Cloud (2)
      • K8S & Docker (4)
      • Linux (3)
      • Next.js & React (3)
      • SpringBoot (2)
      • OS (0)
      • Network (1)
      • AWS (1)
      • Git (1)
      • OpenSource (1)
      • 회고록 (1)
      • 기술세션 공부하기 (0)
      • Certi (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    NCP
    Loki
    uvicorn
    ReactHookForm
    k8s
    오픈소스기여
    ec2느릴때
    ncloud
    githook
    리눅스시스템프로그래밍
    AWS
    네이버클라우드
    Helm
    http응답느릴때
    nks
    네이버클라우드플랫폼
    controlledInput
    rds이관
    RDS
    uncontrolledinput
    springboot
    HikariCP
    objectstorage
    Husky
    promtail
    aws이관
    ec2
    Nclouder
    cronjob
    Grafana
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
jjungking
[NextJS] App router에서 Emotion을 권장하지 않는 이유와 React Streaming SSR을 곁들인
상단으로

티스토리툴바