React 실행 환경과 CSR/SSR 차이 정리
React를 공부하다 보니 걸리는 질문이 쌓였다. React는 프론트엔드 라이브러리인데 어떻게 서버에서도 실행될까. CSR과 SSR은 정확히 뭐가 다를까. 서버에서는 React로 대체 뭘 하는 걸까. 렌더링 방식부터 파고들기 전에 React가 무엇인지를 먼저 붙잡아야 했다.
DOM을 직접 손대는 코드부터 다시 봤다
React가 없어도 JavaScript로 화면은 만들어진다. 카운터 하나라면 마크업은 이 정도다.
<h1 id="count">0</h1>
<button id="button">+1</button>버튼을 누를 때마다 DOM을 찾아서 텍스트를 직접 바꿔주면 동작한다.
let count = 0;
const button = document.querySelector("#button");
button.addEventListener("click", () => {
count++;
document.querySelector("#count").textContent = count;
});애플리케이션이 커질수록 개발자가 직접 관리해야 할 DOM 변경이 늘어난다. 데이터가 하나 바뀔 때마다 매번 같은 절차를 따라가야 한다.
flowchart LR
A[데이터 변경] --> B["어떤 DOM을 찾을지 결정"]
B --> C["어떤 부분을 바꿀지 결정"]
C --> D[DOM 변경]
React는 이 절차를 개발자가 밟게 하는 대신 다른 걸 코드로 적게 한다. "현재 데이터와 상태가 이렇다면 화면은 이렇게 생겨야 한다"는 관계다.
상태와 화면의 관계만 적으면 나머지는 React가 한다
같은 카운터를 React로 옮기면 DOM을 찾는 코드가 사라진다.
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</>
);
}여기 적혀 있는 건 count가 0이면 화면에 0, 1이면 1이 보인다는 관계뿐이다. 개발자가 DOM을 찾아 수정하는 대신, 상태가 변하면 그 상태에 맞는 UI가 무엇인지를 React가 처리한다.
React에서 UI를 표현한다는 건 화면이 어떻게 생겨야 하는지를 JavaScript 코드로 쓴다는 뜻이다. HTML 파일이라면 아래처럼 썼을 마크업이 있다.
<div>
<h1>김민수</h1>
<button>로그아웃</button>
</div>React에서는 같은 마크업이 함수 하나 안으로 들어간다.
function User() {
return (
<div>
<h1>김민수</h1>
<button>로그아웃</button>
</div>
);
}여기서 <h1>김민수</h1>은 HTML 파일에 직접 작성한 HTML이 아니다. JavaScript 코드 안에서 JSX 문법으로 어떤 UI를 보여줄지 표현한 것이다. React에서는 이런 단위를 컴포넌트(Component)라고 부른다.
React는 화면 그 자체가 아니었다
React는 브라우저도, HTML도, DOM도, 화면도 아니다. UI를 코드로 표현하고 관리하기 위한 JavaScript 라이브러리고, 실제로 화면을 보여주는 건 브라우저다.
| 환경 | 가진 기능 | 없는 것 |
|---|---|---|
| 브라우저 | JavaScript 실행 환경, HTML 파서, DOM, CSS 엔진, 화면을 그리는 기능 | - |
| Node.js | JavaScript 실행 환경, 파일 시스템, 네트워크 기능, 서버 기능 | 일반적으로 document, window, 브라우저 DOM |
JavaScript는 프로그래밍 언어고, 실행하려면 실행 환경이 필요하다. 그 역할을 브라우저만 하는 게 아니다.
flowchart TD
Chrome[Chrome] --> Engine1["JavaScript 엔진"]
Node["Node.js"] --> Engine2["JavaScript 엔진"]
Engine1 --> Run1["JavaScript 실행"]
Engine2 --> Run2["JavaScript 실행"]
Node.js가 있으면 서버에서도 JavaScript를 실행할 수 있다. React도 결국 JavaScript로 만들어진 라이브러리다. 그리고 React의 핵심적인 UI 계산 기능 자체가 반드시 브라우저의 document나 window를 필요로 하는 것은 아니다.
function App() {
return <h1>Hello</h1>;
}이 코드에는 document, window, localStorage 같은 브라우저 전용 API가 없다. JavaScript를 실행할 수 있는 환경이라면 React의 일부 기능도 실행할 수 있다.
렌더링을 그리기가 아니라 계산으로 읽었다
렌더링이라는 말을 화면에 무언가를 그리는 것으로 받아들이고 있었는데, React를 이해할 때는 다르게 읽어야 했다. React에서 렌더링은 컴포넌트가 표현하는 UI가 무엇인지 계산하는 과정이다.
위 App 컴포넌트를 렌더링하라고 하면 React는 컴포넌트를 실행하고 그 컴포넌트가 표현하는 UI를 계산한다. 그 계산 결과를 어디에 어떻게 쓰는지는 환경에 따라 갈린다.
브라우저에서는 react-dom, 서버에서는 react-dom/server
브라우저에서는 React가 계산한 UI를 브라우저의 DOM에 반영한다. 이때 DOM은 React가 제공하는 게 아니라 브라우저가 제공한다. React와 브라우저 DOM 사이를 연결해주는 역할을 하는 것이 react-dom이다.
서버에는 브라우저 DOM이 없어서 브라우저처럼 DOM을 조작할 수 없다. 대신 React의 서버 렌더링 기능을 쓴다.
import { renderToString } from "react-dom/server";
const html = renderToString(<App />);React가 컴포넌트를 실행하고 HTML 문자열을 만들어낸다. 결과는 개념적으로 <h1>Hello</h1>와 같다. 서버에 실제 DOM이 만들어지는 게 아니다. 그냥 문자열이다.
const html = "<h1>Hello</h1>";그래서 "서버에서 React가 화면을 그린다"라고 표현하면 오해하기 쉽다. 서버에는 브라우저 화면이 없다. 서버에서 React를 실행해 HTML을 만들고, 브라우저가 그 HTML을 파싱해서 화면에 표시한다.
flowchart TD
React["React: UI 계산 / 관리"] --> Browser["브라우저 환경"]
React --> Server["서버 환경"]
Browser --> RD["react-dom"]
RD --> DOM["브라우저 DOM"]
DOM --> Screen1["화면 표시"]
Server --> RDS["react-dom/server"]
RDS --> HTMLString["HTML 문자열"]
HTMLString --> Parse["브라우저가 HTML 파싱"]
Parse --> Screen2["DOM 생성 후 화면 표시"]
이 그림은 시간 순서가 아니라 React가 서로 다른 환경에서 사용될 수 있다는 관계를 나타낸다. 실제 요청 순서는 CSR인지 SSR인지에 따라 달라진다.
CSR에서는 브라우저가 초기 UI를 만든다
CSR(Client-Side Rendering)에서는 브라우저에서 React가 초기 UI를 만든다.
sequenceDiagram
participant B as 브라우저
participant S as 서버
B->>S: 페이지 요청
S-->>B: HTML과 JS 전달
B->>B: JavaScript 다운로드 후 React 실행
B->>S: API 요청
S-->>B: 데이터 응답
B->>B: React가 UI 생성 후 DOM 업데이트
B->>B: 화면 표시
서버가 보내는 HTML에는 실제 내용이 거의 없을 수 있다.
<body>
<div id="root"></div>
<script src="/app.js"></script>
</body>이후 브라우저가 JavaScript를 실행하면서 React가 UI를 만든다.
SSR에서는 서버가 초기 HTML을 만든다
SSR(Server-Side Rendering)에서는 서버에서 React가 초기 HTML을 만든다.
sequenceDiagram
participant B as 브라우저
participant S as 서버
B->>S: 페이지 요청
S->>S: React 실행 후 UI 계산
S->>S: HTML 생성
S-->>B: HTML 전달
B->>B: HTML 파싱 후 DOM 생성
B->>B: 화면 표시
서버에서 상품 데이터를 가져온 뒤 이 컴포넌트를 실행한다.
function Product({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}원</p>
</div>
);
}그러면 값이 채워진 HTML이 만들어져 브라우저로 전달된다.
<div>
<h1>아이폰</h1>
<p>1,500,000원</p>
</div>갈리는 지점은 초기 UI를 누가 만드느냐였다
CSR과 SSR 모두 HTML과 JavaScript를 쓴다. 가장 중요한 차이는 초기 UI를 누가 만들어내느냐다.
flowchart TD
CSR["CSR"] --> C1["서버: HTML + JS 전달"]
C1 --> C2["브라우저: React 실행"]
C2 --> C3["브라우저: UI 생성"]
SSR["SSR"] --> S1["서버: React 실행 후 HTML 생성"]
S1 --> S2["브라우저: HTML 파싱"]
S2 --> S3["브라우저: UI 표시"]
CSR이라고 해서 HTML을 전혀 보내지 않는 것은 아니다. CSR에서도 HTML은 보내지만, 초기 HTML에 실제 UI가 거의 없을 수 있다.
<div id="root"></div>SSR에서는 실제 UI가 포함된 HTML을 보낸다.
<div id="root">
<h1>아이폰</h1>
<p>1,500,000원</p>
</div>둘 다 HTML을 주고받는다. 갈리는 지점은 초기 HTML을 누가 만들어서 채우느냐다.
서버는 생성하고 브라우저는 파싱한다
생성과 파싱을 구분하고 나서야 서버의 역할이 정리됐다. SSR에서 서버는 React를 실행해 HTML을 생성하고, 브라우저는 그 HTML을 파싱해 DOM을 만들고 화면에 표시한다. 서버가 HTML을 파싱하는 게 아니다.
CSR에서 서버는 빌드 과정에서 만들어진 JavaScript, CSS 등의 파일을 브라우저에 전달한다. React 코드가 빌드 도구를 거쳐 app.js 같은 파일이 되고, 사용자가 요청하면 서버가 그 파일을 넘긴다. 브라우저는 그걸 실행하면서 React를 돌린다. SSR에서는 요청이 들어왔을 때 서버가 React를 실행해 HTML을 만들어 전달할 수도 있다.
React 자체를 먼저 붙잡고 나니 CSR과 SSR이 별개의 개념으로 보이지 않았다. CSR과 SSR은 React의 결과를 어디에서 어떤 방식으로 만들어 사용하는가라는 렌더링 전략의 차이였다.