환경 고민없이 개발하기

김동현

Frontend Developer

2023. 9. 1

서론

토스 프론트엔드 챕터는 유저가 경험하는 로딩 시간을 줄이기 위해 지속적으로 노력하고 있습니다. 특히 Slash 22를 통해 서버 사이드 렌더링(SSR)를 이용한 개선 사례를 소개드린 적이 있는데요. 이번 아티클에서는 Next.js 도입 과정에서 마주한 문제와 해결 방법에 대해 설명드리겠습니다.

서버 사이드 렌더링

서버 사이드 렌더링(SSR)은 렌더링 작업 일부를 서버에 위임하는 방식으로, 브라우저에게 완성된 HTML을 전달합니다. 이를 통해 사용자는 빠르게 서비스를 이용할 수 있고, 해당 서비스는 검색 엔진 최적화(SEO)를 통해 더 많은 노출 기회를 얻을 수 있습니다.

하지만 서버 사이드 렌더링(SSR)을 위해서는 별도의 서버 운영이 필요합니다. 프레임워크를 사용하는 경우, 서버 구축 및 운영 등의 문제에는 벗어날 수 있지만 렌더링 과정에 서버가 개입되면서 window is not defined 와 같은 생소한 에러를 경험하게 됩니다.

단순하게 생각해보면 서버에서 제공한 HTML을 이용한 것뿐인데, 왜 이런 에러를 경험하게 되는걸까요? 서버 사이드 렌더링(SSR)환경에서 흔히 발생하는 에러와 그 해결법을 사례를 통해 살펴보겠습니다.

Next.js 렌더링 과정

예시로 살펴볼 애플리케이션은 쿼리 파라미터로 전달받은 유저의 이름을 화면에 출력합니다.

function App() {
	// 쿼리 파라미터로 전달받은 유저의 이름을 얻어온다.
	const name = new URL(location.href).searchParams.get(name);	
	// 유저의 이름을 화면에 출력한다.
	return <div>{name}</div>
}

코드를 살펴보면 문제없이 동작할 것 같지만 서버 사이드 렌더링(SSR) 환경에서 에러가 발생합니다. 어떤 부분이 에러를 일으키는걸까요? 에러 메시지를 보며, 원인을 찾아보겠습니다.

1. 서버가 HTML을 생성한다.

메세지를 살펴보면, 에러가 발생한 환경은 다음과 같은 특징을 가지고 있습니다.

  1. 브라우저 객체인 location 이 존재하지 않는다. (location is not defined )
    💡 
    location
    브라우저 환경에서 제공되는 객체로, URL 관련 속성 메소드를 제공합니다
    
    
  2. 페이지(HTML) 생성이 가능하다. ( This error happened while generating the page. )

즉 1) 브라우저가 아니면서 동시에 2) 페이지(HTML) 생성이 가능하다는 사실을 통해 서버에서 발생한 에러임을 추측 해볼 수 있습니다.

서버는 클라이언트에서 제공한 컴포넌트를 기반으로 HTML을 생성합니다. 이 때 만약 클라이언트 환경에만 존재하는 코드가 있다면 어떤 일이 일어날까요? 서버는 해당 코드의 작동 방식 을 이해할 수 없고, 이로 인해 에러가 발생하게 됩니다.

처음 작성한 코드를 다시 돌아가보면 location 은 클라이언트 환경에만 존재하는 브라우저 객체입니다. 따라서, 해당 에러를 해결하기 위해서는 서버 환경에서 location 에 접근할 수 없도록 수정해야 합니다.

function App() {
	const name = (() => {
		/* 서버 환경인 경우, 객체에 접근하지 못하도록 수정 .. */
		if ( isServer() ) {
			return null;
		}
		 return new URL(location.href).searchParams.get(name);	
	})();
	/* 유저의 이름을 화면에 출력한다 ..*/
	return <div>{name}</div>
}

2. Hydration Mismatch

서버 환경에서 브라우저 객체에 접근할 수 없도록 수정한 후, 새로운 에러가 발생하였습니다.

위 에러를 해결하기 위해서는 Hydration 에 대한 이해가 필요합니다.

서버에서 생성한 HTML은 단순 마크업이므로 사용자 인터랙션이 불가능합니다. 따라서 React는 이벤트 리스너, 상태 관리와 같은 클라이언트 로직을 전달받은 HTML과 통합하여 애플리케이션으로 작동할 수 있도록 합니다. 이 과정을 Hydration 이라 합니다.

여기서 주의깊게 봐야할 점은 로직 연결 과정입니다. React는 요소(Element)와 로직 정보가 담긴 가상 DOM을 생성한 뒤, 이를 전달받은 HTML과 비교합니다. 따라서, 서버와 클라이언트의 렌더링 결과가 같은 경우에만 Hydration 을 수행할 수 있습니다.

첫 번째 수정사항으로 서버에서 바라보는 name 변수의 값은 항상 null 입니다. 따라서 쿼리 파라미터가 존재하는 경우, 서버와 클라이언트는 각각 다른 결과물을 렌더링 하게 되면서 Hydration 을 수행할 수 없는 상태가 됩니다.

// 서버
<div>{null}</div>

// 클라이언트
<div>{'김토스'}</div>

하나의 코드, 동일한 결과 Isomorphic

그렇다면 어떻게 문제를 해결할 수 있을까요?

Hydration Mismatch 를 해결하기 위해서는, 서버와 클라이언트의 렌더링 결과물이 같아야합니다. 이를 위해 서버 환경에서 쿼리 파라미터에 접근할 수 있는 별도의 로직 작성이 필요합니다.

function App() {
	const name = (() => {
		if (isServer()) {
	    /* 서버 환경에서 쿼리 파라미터 접근 및 반환하는 별도 로직... */
		}
		 return new URL(location.href).searchParams.get(name);	
	});
	return <div>{name}</div>
}

다행히도 Next.js는 개발자가 겪을 불편함을 줄여주고자 useRouter() 을 제공하고 있습니다.

import { useRouter } from 'next/router';

function App() {
	const name = useRouter().query.name;

	return <div>{name}</div>
}

useRouter() 를 사용하면 별도의 예외처리 없이도 서버, 클라이언트 어떤 환경에서든 동일한 결과 값을 보장 받을 수 있습니다.

// 서버
<div>{'김토스'}</div>

// 클라이언트
<div>{'김토스'}</div>

이처럼 서버와 클라이언트 양측에 동일한 결과를 보장하는 코드를 isomorphic 하다고 표현합니다.

요구사항을 다시 살펴보면, 쿼리파라미터 값을 화면에 출력하는 매우 간단한 작업입니다. 그러나 서버 사이드 렌더링 환경에 대한 이해가 없었다면, 에러를 해결하는데 많은 시간을 소비했을 것입니다. 만약 처음부터 useRouter() 를 사용했다면 어땠을까요?

isomorphic 한 코드는 나와 동료의 시간을 절약해줍니다. 일관된 결과를 서버와 클라이언트 양측에 보장하기 위해서는 관련 작업이 반드시 필요합니다. 따라서, 이러한 작업들을 추상화 해둔다면 불필요한 코드들을 감춰지고, 구현에만 집중할 수 있게 됩니다.

토스의 isomorphic

그렇다면 실제 서비스에 적용해볼 수 있는 실용적인 사례는 없을까요? 토스 프론트엔드 챕터에서 사용하고 있는 isomorphic 사례를 소개드리고 마무리 하겠습니다.

  1. SSRSuspense

<Suspense /> 는 비동기 요청을 선언적으로 처리할 수 있도록 돕는 컴포넌트입니다.

  • Promise가 대기 상태일 때(Pending) : <Loading />
  • Promise가 완료됐을 때(Resolved): <APIRequestComponent />
function App() {
	return (
		<Suspense fallback={<Loading/>}>
			<APIRequestComponent />
		</Suspense>
	)
}

그러나 React 18 버전 미만에서는 <Suspense/>가 오직 클라이언트 환경에서만 정상 작동한다는 한계점이 있습니다. SSR 환경에서 안정적으로 작동할 수 있도록 <Suspense/>는 컴포넌트가 마운트 되기 전에는 fallback 컴포넌트를 렌더링합니다.

재미있게 읽으셨나요?

좋았는지, 아쉬웠는지, 아래 이모지를 눌러 의견을 들려주세요.

😍
🤔

토스팀이 만드는 수많은 혁신의 순간들

당신과 함께 만들고 싶습니다.
지금, 토스팀에 합류하세요.
채용 중인 공고 보기