안녕하세요, 프론트엔드 개발자 오소현입니다.
Next.js처럼 서버 사이드 렌더링(SSR)을 지원하는 프레임워크를 사용하다 보면, 다음과 같은 에러를 한 번쯤 마주친 경험이 있을 것입니다.
ReferenceError: window is not defined
이 에러는 단순한 코드 오류가 아니라, 클라이언트와 서버의 실행 환경 차이에서 비롯된 문제입니다. 오늘은 이 에러가 왜 발생하는지, 그리고 이를 해결할 수 있는 isomorphic 코드 작성 방식에 대해 이야기해보겠습니다.
SSR(Server Side Rendering)은 사용자가 페이지에 접속하면 서버가 React 컴포넌트를 실행해 완성된 HTML을 만든 뒤, 이를 브라우저에 전달하는 방식입니다. 브라우저는 이 HTML을 곧바로 렌더링할 수 있기 때문에 초기 로딩 속도가 빠르고, 검색 엔진에 노출될 수 있는 HTML 콘텐츠가 존재하므로 SEO 측면에서도 매우 유리합니다. Next.js는 이러한 SSR을 프레임워크 차원에서 지원하여 사용자 경험과 검색 최적화를 동시에 충족시킬 수 있게 돕습니다.
SSR은 Node.js 기반의 서버 환경에서 실행되며, 이 환경에는 window,
document
, location
과 같은 브라우저 전용 객체가 존재하지 않습니다. 그러나 프론트엔드 개발자는 이러한 객체에 익숙해 브라우저 환경을 전제로 코드를 작성하는 경우가 많습니다.
예를 들어 다음과 같은 코드를 서버에서 실행하면 에러가 발생합니다.
const name = new URL(location.href).searchParams.get('name');
// SSR 환경에서는 location is not defined 에러 발생
이처럼 브라우저 전용 객체를 사용하는 코드는 서버 환경에서 실행될 수 없기 때문에, window is not defined, location is not defined
등의 에러가 발생하게 됩니다.
SSR을 통해 전달된 HTML은 정적인 형태로, 사용자와의 상호작용은 불가능한 상태입니다. React는 이 HTML에 JavaScript 로직과 상태를 연결해 인터랙션이 가능한 형태로 전환하는데, 이 과정을 Hydration이라고 합니다.
문제는 서버가 생성한 HTML과 클라이언트에서 만들어지는 가상 DOM이 일치하지 않을 경우 발생합니다. 이때 React는 Warning: Text content did not match
라는 경고를 출력하며, 이를 Hydration Mismatch라고 합니다. 보통 서버와 클라이언트가 서로 다른 데이터를 기반으로 렌더링할 때 이러한 문제가 발생합니다.
Next.js와 같은 SSR 프레임워크에서는 서버에서 렌더링된 HTML과 함께 초기 데이터를 직렬화해 브라우저로 전달합니다. 이 직렬화된 데이터는 클라이언트 측 Hydration 과정에서 사용되며, 서버와 클라이언트의 UI를 일치시키는 데 핵심적인 역할을 합니다.
SSR의 실행 흐름을 다시한번 더 잡고 가보겠습니다.
서버는 React 컴포넌트를 실행하여 HTML 문자열을 생성합니다.
이와 함께 필요한 초기 상태(props)를 JSON 형태로 <script id="__NEXT_DATA__">
에 직렬화해 포함합니다.
브라우저는 전달받은 HTML을 먼저 렌더링하고, 직렬화된 데이터를 바탕으로 ReactDOM.hydrate()
를 실행합니다.
클라이언트는 기존 HTML과 동일한 구조의 가상 DOM을 생성하여 상호작용이 가능한 React 앱으로 전환됩니다.
예시로 함께 살펴볼까요?
export async function getServerSideProps(context) {
return {
props: {
serverTime: new Date().toISOString(),
},
};
}
export default function Home({ serverTime }) {
return <div>Server time: {serverTime}</div>;
}
이 코드는 서버 시각을 기반으로 렌더링된 HTML을 생성하며, 그 결과는 다음과 같이 직렬화된 데이터를 포함하게 됩니다.
<div id="__next">
<div>Server time: 2025-06-28T07:00:00.000Z</div>
</div>
<script id="__NEXT_DATA__" type="application/json">
{
"props": {
"pageProps": {
"serverTime": "2025-06-28T07:00:00.000Z"
}
}
}
</script>
클라이언트는 이 JSON 데이터를 바탕으로 동일한 내용을 Hydration하게 되어 서버와 클라이언트의 UI 일치성을 보장합니다.
반면, 아래와 같은 코드는 에러를 리턴합니다.
export default function Home() {
const now = new Date().toISOString();
return <div>Client time: {now}</div>;
}
이 코드는 서버와 클라이언트의 렌더링 결과가 달라져 Hydration mismatch 경고가 발생할 수 있습니다. 따라서 서버-클라이언트 간 동일한 데이터 기반 렌더링이 중요합니다.
이러한 문제들을 방지하기 위한 접근 방식이 바로 Isomorphic 코드 작성입니다. Isomorphic은 서버와 클라이언트에서 동일한 코드를 실행하더라도 같은 결과를 보장하는 코드를 말합니다. SSR 환경에서는 브라우저 전용 객체를 무분별하게 사용하거나, 서로 다른 데이터로 렌더링하는 경우 다양한 에러가 발생할 수 있기 때문에, 이러한 상황을 방지하기 위해 Isomorphic한 코드 작성이 중요합니다. 동일한 데이터를 바탕으로 양쪽에서 일관된 렌더링을 수행하면, 서버 환경에서도 에러 없이 코드를 실행할 수 있고 Hydration Mismatch도 방지할 수 있습니다.
어디서 실행하든 동일한 결과를 낼 수 있다는 것이 Isomorphic의 핵심 개념입니다.
예를 들어 다음 코드는 브라우저 환경에만 존재하는 location
객체를 직접 참조하고 있습니다.
function App() {
const name = new URL(location.href).searchParams.get('name');
return <div>{name}</div>;
}
이 코드는 서버 사이드 렌더링 시 location
객체가 존재하지 않아 location is not defined
에러가 발생하게 됩니다. 이러한 문제를 해결하기 위한 개선 방법은 크게 두 가지입니다.
function App() {
let name = null;
if (typeof window !== 'undefined') {
const url = new URL(window.location.href);
name = url.searchParams.get('name');
}
return <div>{name}</div>;
}
이 코드는 클라이언트 환경에서만 window
객체를 참조하므로 SSR 에러를 방지할 수 있습니다. 하지만 서버에서는 name
이 null로 렌더링되어 SEO에 불리할 수 있습니다.
보다 완전한 방식은 getServerSideProps
를 활용해 서버에서 데이터를 추출한 뒤 컴포넌트에 전달하는 것입니다.
export async function getServerSideProps(context) {
const { name } = context.query;
return {
props: {
name: name || null,
},
};
}
function App({ name }) {
return <div>{name}</div>;
}
이 방식은 브라우저 전용 객체에 의존하지 않고, 서버와 클라이언트가 동일한 데이터를 기준으로 렌더링하므로 Hydration mismatch 없이 안정적인 SSR이 가능합니다.
React의 <Suspense />
는 비동기 데이터를 처리할 때 유용한 기능입니다. 데이터를 가져오는 동안 fallback
UI를 보여주고, 데이터가 준비되면 본 컴포넌트를 렌더링합니다. React 17까지는 <Suspense />
가 서버 환경에서 제대로 작동하지 않아 SSR에서는 주의가 필요했지만, React 18부터는 서버 환경에서도 자연스럽게 작동할 수 있도록 개선되었습니다. 그 결과 SSR 환경에서도 안정적으로 비동기 렌더링과 로딩 처리가 가능해졌고, 이는 Isomorphic한 렌더링 흐름을 구현하는 데 큰 도움이 됩니다.
개발자 입장에서는 SSR 환경에서만 발생하는 에러들을 원천적으로 차단할 수 있고, 클라이언트와 서버 사이의 렌더링 불일치를 줄여 사용자에게 보다 안정적인 화면을 제공할 수 있습니다. 또한 팀원들이 일관된 기준으로 코드를 작성하게 되어 코드의 유지보수성과 협업 효율성도 높아집니다.
결국 서버와 클라이언트의 실행 환경 차이로 인해 발생할 수 있는 다양한 문제를 사전에 방지하고, 사용자에게 일관된 경험을 제공하며, 개발자 자신도 예측 가능한 코드를 다룰 수 있도록 돕는 것이 Isomorphic 작성 방식의 핵심 가치라고 할 수 있습니다. SSR 환경에서의 안정적인 개발을 위해 반드시 고민해봐야 할 접근 방식이라고 볼 수 있습니다!
Isomorphic 가 이런 뜻이 있었군요! 하나 배워갑니다