본문 바로가기
Front-End/웹 (Web)

React(SPA)에서 Next.js(SSR)를 써야하는 이유

by MS_developer 2023. 7. 21.

 

원티드에서 주관하는 7월 프리온보딩 프론트엔드 챌린지에 참가하게 되었다.

 

참가하기에 앞서 직접 해보길 권장하는 사전 과제가 있었다.

 

1. CSR(Client-side Rendering)이란 무엇이며, 그것의 장단점에 대하여 설명해주세요
2. SPA(Single Page Application)로 구성된 웹 앱에서 SSR(Server-side Rendering)이 필요한 이유에 대하여 설명해주세요.
3. Next.js 프로젝트에서 yarn start(or npm run start) 스크립트를 실행했을 때 실행되는 코드를 Next.js Github 레포지토리에서 찾은 뒤, 해당 파일에 대한 간단한 설명을 첨부해주세요.

 

과제를 진행하면서 유익한 공부가 되었기에 해당 과제와 관련된 지식들을 정리하고, 이후에도 참고하기 위해 글로 기록해 놓으려고 한다. (많이 늦었지만 🥲)

 


브라우저가 작동하는 방식 (Critical Render Path)

 

개발자가 아닌 대부분의 사람들은 인터넷, 정확히 말하자면 브라우저가 어떤 방식으로 작동되는지 친숙하지 않을 것이다.

 

 

아이콘을 누르면 브라우저 창이 뜨고, 짧은 시간 안에 화면이 출력된다. 심지어 정보가 담겨있다!

 

그야말로 기이한 현상이다.

 

도대체 브라우저는 어떻게 작동하기에 이토록 빠른 시간 안에 우리와 통신하고 정보를 출력할까?

 

출처: How Browser Works (하단 참조)

 

브라우저는 위 이미지와 같은 구조(Architecture)를 가지고 있는데, 사용자가 상호 작용 시 브라우저 엔진 - 렌더링 엔진의 순서를 거쳐 웹 페이지를 화면에 표시한다.

 

실제로는 좀 더 복잡한 과정을 거치지만, 우리가 보고 있는 화면에 정보를 표시하는 핵심은 렌더링 엔진(Rendering Engine)에 있다.

 

MDN 공식 문서에서는 렌더링 엔진을 "화면에 이미지와 텍스트를 가져오는 소프트웨어 (rendering engine is software that draws text and images on the screen)"라 정의하고 있다.

 

출처: How Browser Works (하단 참조)

 

정의 자체는 간단하지만 실제로는 조금 더 복잡한 과정을 거쳐 웹 페이지가 렌더링 되는데, 이 과정을 중요 렌더링 경로 (Critical Rendering Path)라는 개념으로 시각화할 수 있다. 

 

중요 렌더링 경로의 과정은 다음과 같다.

 

1. 브라우저에서 HTML과 CSS 파일들을 파서(Parser)로 읽어와 의미 단위로 토큰화(Tokenization)한 후 각각 DOM(Document Object Model)과 CSSOM(Cascading Style Sheet Object Model)으로 구성

2. DOM과 CSSOM 두 트리를 합쳐 실제 화면을 그리기 위한 정보인 렌더 트리(Render Tree)로 변환

3. 렌더 트리를 기반으로 각 요소가 위치할 자리(Layout)를 계산하고, 구성된 화면을 출력(Painting). 해당 과정에서 렌더 트리에 변화가 생기면 그 변화의 범위에 따라 요소가 그려질 범위를 다시 계산하거나, 화면에 그립니다.

 

다소 복잡한 과정들이 포함되어 있지만, 핵심은 HTML과 CSS로부터 문서의 구조와 스타일을 읽어온 뒤, 하나의 화면으로 그려내는 과정임에 있다.

 

위 과정을 온전히 이해하고 있어야 CSR(Client-Side Rendering)SSR(Server-Side Rendering)에 대한 개념을 이해할 수 있다.


SPA(Single Page Application)와 CSR(Client-Side Rendering)

 

출처: github react_spa (참조)

 

SPA(Single Page Application)인 React는 CSR(Client-Side Rendering) 방식으로 동작한다.

 

리액트에 대해 공부하면 알 수 있는 가장 기본적인 내용이다. 이게 정확히 무슨 말일까?

 

앞서 언급했듯 브라우저는 웹 페이지에 대한 정보가 담겨 있는 파일을 불러와 일련의 과정을 거쳐 화면을 렌더링 한다. 

 

CSR(Client-Side Rednering)SSR(Server-Side Rendering)은 이때 해당 파일을 렌더링 하는 주체가 누구인지에 대해 정의하고 있다. 즉, CSR은 클라이언트(브라우저)가 렌더링의 주체가 되는 방식이고 SSR은 서버가 렌더링의 주체가 되는 방식을 말한다.

 

리액트를 통해 웹 페이지를 개발하면 사용자는 여러 페이지를 이동하는 것처럼 느껴도 실제로는 하나의 페이지에서 사용자의 상호작용(interactiton)에 따라 준비된 다른 컴포넌트를 재렌더링하고 있다. 즉, 화면을 출력하는 렌더링 과정이 서버가 아닌 브라우저(클라이언트)에서 맡아서 진행되고 있음을 알 수 있다.

 

그렇기 때문에 리액트는 하나의 페이지로 구동되는 애플리케이션(Single Page Applicationn)이고, CSR 방식으로 렌더링 한다고 말한다. 

 

그렇다면 리액트는 왜 기존 방식(SSR)과 차별성을 둔 CSR 방식을 채택했을까?

 

바로 CSR 방식이 제공할 수 있는 강점들에 주목했기 때문이다.

 

빠른 상호작용

 

위 예시처럼 CSR을 채택한 웹 페이지는 사용자 상호작용이 빠르다. 페이지 전환이 로딩 없이 되기 때문에 사용자가 불편함을 적게 느끼고, 이는 사용자가 해당 웹 페이지를 사용함에 있어 피로도를 적게 느낄 수 있다는 말과 같다. 즉, 사용자가 해당 웹 페이지를 더 길게 이용할 수 있다.

 

2023년 기준 웹 페이지의 로딩 시간은 "최대" 3초를 넘기지 않는 것이 좋다고 한다. (참조)

 

개발자가 돈 받고 웹 페이지를 만드는 이유가 무엇일까?

 

쇼핑몰, 관공서 등 특정한 사용자가 해당 웹 페이지에서 제공하는 기능과 편의성을 활용해 웹 페이지의 목적(수익 창출, 편의 제공 등)을 달성하기 위함이다.

 

비즈니스적 관점에서 사용자가 조금이라도 오래 남아있을 수 있다면 해당 웹 페이지의 목적을 완수할 확률도 올라간다. 때문에 좋은 UX를 제공하는 것은 웹 페이지 개발에 있어 매우 중요한 요소다.

 

서버 부하 감소

 

CSR 방식을 채택했다고 해서 서버와 한 번 통신하고 끝나지 않는다.

 

API에서 실시간으로 사용자에게 필요한 데이터를 가져와 업데이트 시켜줘야 하기 때문이다.

 

사진 출처: 8 Best Examples of Ecommerce Shopping Cart Page Designs (참조)

 

쇼핑몰을 예로 들자면, 우리가 장바구니에 물건을 추가했을 때 기존에 불러왔던 상품 이미지와 내용들과는 별개로 해당 상품에 대한 정보가 장바구니에 추가되어야 한다.

 

만약 장바구니에 물건이 추가되었다고 페이지 전체를 다시 로딩해야 된다면 데이터 손실이 클 것이다.

 

CSR은 이때, 상품에 대한 정보만을 받아오고 해당 부분(컴포넌트)에만 변경 사항을 적용하기 때문에 전체 페이지가 새로고침되지 않아도 된다.

 

개발 효율 증가

 

CSR 방식은 클라이언트와 서버의 역할을 분리하기 때문에 개발자가 프론트엔드와 백엔드를 독립적으로 개발할 수 있다.

 

따라서 JavaScript 프레임워크 및 라이브러리(React, Vue.js, Angular 등)를 사용해도 백엔드 개발자가 해당 코드를 보고 혼동을 겪을 일이 거의 없다.

 

또한, 이미 만들었던 컴포넌트를 다른 페이지 또는 루트에 서버 통신 없이도 재사용할 수 있다. 해당 컴포넌트로 인해 문제가 발생했을 시 프론트엔드 선에서 해결할 수 있는 건 덤. 즉, 개발자들이 보다 빠르게 웹 페이지를 개발할 수 있다는 말과 같다.

 

앞서 언급했듯 웹 페이지를 만들 때 비즈니스적 관점을 무시할 수 없는데, 개발 효율이 증가한다는 말은 비용이 적게 든다는 말과 같다.

 

개발자에겐 좀 더 적은 스트레스를, 경영자에겐 보다 효율적인 투자를 할 수 있게 도와준다.

 

 

이 외에도 많은 장점들이 있는데, 참으로 좋은 렌더링 방식이 아닐 수 없다.

 

하지만 큰 힘(?)에는 대가가 따르는 법... CSR 방식은 단점도 확실히 존재한다.

 

 

 

초기 로딩 속도 문제

 

CSR은 사용자가 처음으로 페이지에 방문했을 때 클라이언트에서 필요로 하는 파일들(HTML, CSS, JavaScript)을 모두 다운로드하고 렌더링 과정을 거쳐야 되기 때문에 기존 SSR 방식보다 첫 페이지의 로딩 속도가 느리다.

 

특히 웹 페이지의 규모가 커지고 복잡해질수록 이 문제가 심각해진다.

 

뭐야 내 페이지 돌려줘요

 

CSR 방식은 별도의 로딩 화면을 준비하지 않는다면 초기 페이지를 로딩하는 동안 위와 같은 화면을 맞이하게 된다.

 

앞서 언급했듯, 사용자에게 좋은 UX를 제공하기 위해 다양한 방식이 시도되고 있는데... 오히려 초기 로딩 페이지에서 사용자가 큰 불편함을 느낀다면 본말전도가 아닐까? 

 

 

검색엔진(SEO) 최적화 문제

 

반복해서 언급되고 있는 비즈니스적 관점을 다시 한번 꺼내보자.

 

온라인 쇼핑몰을 운영하고 수익을 창출하려면, 가장 필요한 것이 무엇일까?

 

바로 상품을 구매할 "사용자"다.

 

온라인 쇼핑몰을 방문했다면 해당 사용자는 무언가 구매하기 위한 의사를 가지고 있다고 볼 수 있다. 사용자마다 구매하는 품목과 수량이 다르겠지만, 수익을 창출하기 위한 절댓값을 높이려면 결국 "더 많은 사용자"가 필요하다.

 

그렇다면 더 많은 사용자를 불러들이려면 어떻게 할까?

 

웹에서 사람들은 보통 검색을 통해 쇼핑몰을 찾아온다.

 

CSR 방식은 빈 페이지(HTML)에서 시작해  자바스크립트 파일을 다운로드하고,  HTML 파일로 변환하는 과정을 거친다. 때문에 브라우저에서 사용자가 검색했을 때 CSR 방식으로 개발된 웹 페이지는 검색 엔진에게 내용이 별로 없는 페이지로 보여 상위 결과로 노출되기 어렵다.

 

최근 CSR 방식의 웹 페이지를 보다 잘 이해하고 색인화할 수 있는 기술이 검색 엔진에 개발되고 있다고하니 이 단점은 사라질 수도 있지만, 현재까지는 CSR 방식의 문제점이 되고 있다.

 

흠...터레스팅

개발자들은 여기서 생각을 하게 되었을 것이다.

 

"CSR 방식은 좋은데... 단점만 어떻게 못 없애나?"

 


SPA(Single Page Application)로 구성된 웹 앱에 SSR(Server-Side Rendering)을

 

출처: Server Side Rendering vs Client Side Rendering (참조)

 

 

잠시 SSR과 CSR을 비교해 보겠다.

 

기존 방식이라고 설명해 온 SSR 방식은 서버를 주체로 페이지를 렌더링 하기 때문에 CSR 방식과 대척점에 서 있다고 볼 수 있다.

 

예를 들어, CSR이 초기 페이지 로딩이 느리다는 단점이 있다면, SSR은 상대적으로 초기 페이지 로딩이 빠른 편이다. 

 

반대로 CSR은 초기 로딩 페이지 이후에는 사용자 상호 작용에 따른 페이지 전환이 매우 빠르지만, SSR은 첫 페이지를 로딩하는 과정을 똑같이 반복하기 때문에 CSR보다 느리다.

 

앞서 언급했던 CSR의 대표적인 장단점을 SSR과 대조하여 표로 만들어 보았다.

 

CSR SSR
초기 페이지 로딩이 느리지만 이후 페이지 전환이 빠르다. 초기 페이지 로딩이 빠르지만 이후 페이지 전환이 느리다.
서버 자원 사용량이 적어 서버 부담이 적다. 서버 자원 사용량이 많고 서버 부담이 크다.
검색 엔진 최적화(SEO)에 부적합하다. 검색 엔진 최적화(SEO)에 적합하다.

 

매우 간략한 비교지만, 이를 통해 한 가지 생각이 든다.

 

"그럼 처음에만 SSR 방식으로 렌더링 하고 이후에는 CSR 방식으로 하면 안 되나?"

 

앞서 언급했듯이, 개발자들이 생각한 CSR의 단점을 보완하는 방식으로 SSR에 눈길이 가게만 되는 것이다.

 

SPA와 CSR의 개념이 다른 이유가 여기 있다.

 

SPA는 사용자가 루트 변경 시 페이지 이동을 하더라도 하나의 페이지에서 동적으로 페이지를 업데이트하는 방식을 말하는 보다 포괄적인 개념이고, CSR은 페이지를 렌더링 하는 방식에 있어 클라이언트가 주체가 되어 렌더링하는 개념이다.

 

 

하지만 SPA 방식을 유지하기 위해서는 CSR을 채택하는 것이 거의 필수적이다.

 

단, 앞서 언급했듯 첫 페이지 렌더링 시 SSR 방식을 채택하고, 이후 페이지 렌더링은 CSR 방식을 채택하면 SPA 양식을 유지할 수 있다.

 

그리고 이를 통해 CSR의 가장 큰 단점인 초기 페이지 로딩 속도SEO 문제를 개선할 수 있다(!!)

 

 

즉, CSR의 장점만을 채택하고 가장 큰 문제를 해결할 수 있게 되는 "극한의 이득"을 볼 수 있게 되는 것이다.

 

그리고 이 개념을 활용해 리액트에 적용/실현시킨 것이 바로...

 

되시겠다.

 

물론 Next.js 없이도 리액트로 SSR 구현을 할 수 있지만, 비용이 많이 드는 작업이기 때문에 준비되어 있는 Next.js를 활용하는 것이 좋을 듯 싶다.

 


Next.js 리포지토리 뜯어보기

 

SPA로 구성된 앱에 SSR을 도입하는 방식을 알았으니, 다음은 어떻게 Next.js 프로젝트에서 가장 기본적인 명령어인 yarn start (or npm run start)가 구동되는지 알아보았다.

 

 먼저 Next.js의 공식 깃헙 리포지토리를 방문했다.

 

 

 

수많은 파일들이 눈앞을 어지럽혔다.

 

기술 스택의 구동 원리에 대해 자세히 알아보려면 공식문서(Documentation)만 한 것이 없다고 생각해 바로 docs 폴더부터 확인해 보았다.

 

 

눈에 보이는 파일 수가 좀 더 적어져 기본적으로 압박감(?)이 덜 했다.

 

여기서 가장 기본이 되는 첫 번째 파일 docs/01-getting-started/01-installation.mdx 파일을 읽어 보았다.

 

 

해당 문서 내부에서 scripts 명령어에 해당하는 각 기능에 대한 간략한 설명과 해당 기능에 대한 더 자세한 링크를 확인할 수 있었다.

 

현재 찾아보고 있는 명령어는 "start" 명령어였기 때문에 바로 해당하는 파일 경로(docs/02-app/02-api-reference/08-next-cli.mdx)로 들어가 보았다.

 

 

해당 파일 내에서 #production 항목에 해당하는 항목을 찾았다. 

 

해당 항목이 next start 명령어를 담당하고 있으며 next build 과정을 먼저 시작해야 한다는 주의점과 기본 설정된 포트 번호(3000)를 바꿀 수 있는 명령어에 대해 설명되어 있었다.

 

아쉽게도 내가 해당 명령어의 구동 원리는 아니었지만, 무엇을 중점적으로 살펴보아야 하는지 알 수 있었다.

 

 

해당 글은 문서 최상단에 있었는데, Next.js에서 제공하는 CLI(Command-Line Interface: 명령어 인터페이스)에 대해 알아봐야 함을 알 수 있었다. 해당 글이 큰 도움이 된 이유는 깃헙 리포지토리에서 scripts 폴더가 있지만 명령어에 관한 구동 원리를 찾을 수 없었기 때문이다.

 

그렇게 폴더 이곳저곳을 찾아보다가 구동 원리로 짐작이 가는 코드를 찾을 수 있었으니... 해당 파일은 packages/next/src/cli 경로에 있었다.

 

이곳에서 next-start.ts 라는 파일에 대해 읽어보게 되었다.

 

 

해당 코드 일부에서 익숙한 문구("The application should be compiled with \ 'next build\' first.")를 찾을 수 있었다.

 

 

이곳에서 startServer 라는 함수를 사용하는 것을 알 수 있었고, 해당 함수가 상대 경로 '../server/lib/start-server'에서 import 해오고 있음을 알 수 있었다. 즉시 경로를 따라가보니 과련 start-server.ts 라는 파일이 있었다.

 

그리고 이곳에서 startServer의 실체를 보게 되는데...

 

대충 엄청 길다는 뜻

코드가 생각보다 길었다.

 

하지만 찬찬히 뜯어보면 그렇게 긴 코드도 아니었기 때문에 해당 함수를 이해하기 위해 천천히 읽어 내려갔다.

 

 

여기서 주목하게 된 코드는 서버가 생성되는 부분이었는데, 서버 생성이 Node.js에 기본으로 내장되어 있는 http.createServer를 기반으로 작동되고 있음을 알 수 있었다. 완벽하게 코드를 이해하지 못했지만, 서버와 통신하고 이에 대한 응답(res)을 기반으로 선언된 변수 sockets에 추가하는 것을 알 수 있었다. 또한 해당 응답이 종료(on 'close')되는 시점에 sockets을 비우고 있었다. 이는 서버를 효율적으로 관리하기 위한 작업으로 보인다.

 

또한 해당 함수는 try catch 문으로 에러 핸들링을 하는데, 해당 과정에서 현재 포트가 사용 중인지, 에러가 발생한다면 재구동(retry)을 허용할 것인지 등을 설정할 수 있었다.

 

 

위 코드는 서버가 정상적으로 구동 중일 때 사용되는 try catch 문의 일부로, 렌더링 과정을 시작하고 있음을 알 수 있었다. 이 과정에서 모듈 내부의 파일 경로를 입력받아 해당 파일을 가져오는 require.resolve() 함수를 사용해 renderServerPathjestWorkPath 변수를 선언 및 할당한다. jestWorkPath 변수는 최초에는 next/dist/compiled/jest-worker 파일 경로를 통해 가져온 데이터를 기반으로 웹 페이지의 렌더링(SSR)을 시동하고 이전에 불러온 디렉토리가 있다면 해당 디렉토리 (prevDir)을 새롭게 불러오는 디렉토리로 대치하는데, 이는 라우터에 설정된 경로 renderServerPath를 기반으로 한다.

 

사실 내가 이해한 것이 맞는 해석인지 모르겠지만, next.js는 node.js에 기본 내장 되어 있던 함수 또는 모듈들(http.createServer, require.resolve() 등)을 사용해 웹 소켓을 열어 서버를 활성화시키고 서버 관련 옵션들에 대한 관리(핸들러)와 렌더링 방식을 어렴풋이나마 이해할 수 있어 뿌듯했다.


마치며

 

 

처음 사전 과제를 시작했을 때, 문제들에 대한 해답을 찾아가는 과정이 쉽지만은 않았다.

 

하지만 이를 통해 알고 있던 지식 중 미진한 부분에 대해 보완할 수 있었고, 더 자세히 알 수 있어 뜻깊었다. 또한, 블로그 글로 적으면서 다시 한번 기본 개념들에 대해 정리할 수 있었다.

 

앞으로 배울 Next.js가 왜 필요한지, 또 왜 사용되는지, 그리고 현직 개발에서 어떤 부분들을 신경 쓰고 있는지를 생각해 볼 수 있는 좋은 계기가  되었다.

 

특히 리포지토리 내부의 코드를 살펴봄으로써 라이브러리나 프레임워크를 무조건 사용하지 않고 구동 원리를 이해하고 있는 것에 어떤 강점이 있는지 알 수 있는 귀중한 기회였다. 앞으로도 종종 무섭지만 라이브러리나 프레임워크를 살펴보아야겠다는 생각이 들었다.


참조

'Front-End > 웹 (Web)' 카테고리의 다른 글

[Web] 웹 표준과 접근성  (0) 2022.11.20
[HTTP/네트워크] REST API  (0) 2022.11.05
[Web] UI와 UX  (0) 2022.11.03
[HTTP/네트워크] HTTP와 브라우저 작동 원리  (0) 2022.10.19
[HTTP/네트워크] 웹 애플리케이션 구조  (0) 2022.10.17

댓글