
React SEO Strategies and Best Practices의 의역본입니다.
다듬어지지 않은 번역체를 상당수 포함하고 있어서 웬만하면 원문을 읽으시는 것을 추천합니다.
👍 PRS > SSRS > CSRB, PRH > CSR, SSRH 👎
리액트는 선언형, 모듈화, 크로스 플랫폼 인터랙티브 UI를 구성을 위해 만들어졌으며, 오늘날 프론트엔드 필드에서 가장 유명하고 최소 제 임무는 잘 해내는 JavaScript 기반 프레임워크입니다.
초기에는 SPA를 위해 개발되었으나, 이제는 완성형 웹사이트나 모바일 앱을 구축할 수 있을 정도로 발전했습니다.
이러한 요인들과 더불어 리액트의 높은 수요는 SEO 취약성이라는 문제에 직면하게 되었죠.
고전 웹 개발 경험이 있는 개발자라면 리액트를 접했을 때 JavaScript코드가 상당수의 HTML + CSS코드를 대체함을 체감했을 것입니다.
리액트가 직접적으로 UI 요소를 생성 / 업데이트 하는 대신 UI의 state를 활용함으로써 가능한 일입니다.
리액트는 state의 변경에 따라 DOM을 효율적으로 업데이트합니다.
화면 상의 모든 변경들은 리액트 엔진을 통해 이루어집니다.
개발자에게는 편리하겠지만, 이는 유저에게 SEO 문제를 일으키고 이는 곧 검색 효율을 떨어뜨리는 결과를 낳게됩니다.
이 아티클은 리액트 기반 앱 / 웹을 구현 시 고려할 SEO 문제점과 그에 대한 전략을 제시합니다.
온라인 검색 점유율 90%를 웃도는 구글의 크롤링과 인덱싱 과정을 살펴봅시다.
실제 google bot은 아래의 다이어그램보다 훨씬 더 복잡함을 염두하시길 바라며 자세한 내용은 위 스크린샷의 출처인 구글 공식문서를 참고하세요.

a태그를 뽑아내어 크롤 큐에 쌓아올립니다.약 130조 개의 웹페이지를 검토해야하는 구글 봇의 입장을 고려하면 JavaScript 실행 비용을 간과해서는 안되겠지요.
구글봇은 웹페이지를 크롤할 때, HTML을 파싱한 뒤 JavaScript를 큐에 쌓습니다.
페이지 당 렌더 큐에 수 초간 머무르느라 지연 시간이 발생 할지라도 말입니다.
크롤링 예산 (Crawl Budget)에 대해서도 알아보겠습니다.
구글의 크롤링은 대역폭(bandwidth), 시간, 그리고 구글봇 인스턴스 같은 요인에 의해 유한합니다.
구글봇은 일정량의 예산과 리소스를 웹사이트 인덱싱하는데 할애합니다.
만약 쇼핑몰 같은 무거운 페이지를 구축한다면, 당연히 컨텐츠 렌더링을 위해 JavaScript가 많이 쓰일테니 구글은 컨텐츠의 일부만 뽑아올 수 밖에 없을 것 입니다.
앞으로 전개할 설명은 빙산의 일각에 불과합니다.
개발자라면 마땅히 리액트 페이지를 크롤하고 인덱스하는 서치 엔진에 의해 발생할 수 있는 문제들을 파악할 수 있어야합니다.
이제 리액트의 SEO는 왜 번거로운 지, 그리고 이를 극복하기 위해 어떤 노력을 할 수 있는 지 살펴봅시다.
리액트는 JavaScript 기반이며 이로 인해 서치엔진 문제에 직면하는 일이 잦다는 것을 이미 잘 알고 있을 것 입니다.
리액트는 기본적으로 App Shell Model을 채택(employ)하기 때문입니다.
초기의 HTML은 유의미한 컨텐츠를 담고있지 않고, 사용자나 봇은 컨텐츠를 보기위해선 JavaScript를 실행해야 하죠.
이러한 접근법(approach)은 구글봇이 first-pass에서는 비어있는 페이지를 인지함을 의미합니다.
컨텐츠는 페이지 렌더링을 마친 후 그제서야 보여질 수 있기 때문에, 여러 페이지를 가진 사이트는 컨텐트 인덱싱 지연이 생기기 마련입니다.
Fetching, Pasring 그리고 JavaScript를 실행하는 일은 시간을 필요로 합니다.
아마 JavaScript는 부가적인 컨텐츠 fetch를 위해 네트워크 요청을 보낼 것이고, 사용자는 요청 정보가 로드 될 때 까지 기다려야 하겠죠.
구글은 UX를 위해 여러 Core Web Vitals를 제공해왔습니다. Core Web Vital은 구글 랭킹요소에 영향을 줍니다.
컨텐츠 로딩 시간은 UX 점수에 영향을 미치고, 그 결과로 검색 랭킹 순위에서도 밀려나게 되겠지요.
구글이나 소셜 미디어는 메타 태그를 통해 얻은 타이틀 썸네일이나 페이지를 요약한 정보를 데려올 수 있습니다.
하지만 그런 사이트는 정보를 얻기위해서 헤드 태그에 의존할 수 밖에 없습니다. 정작 타겟 페이지에 대한 정보를 로드하기위한 JavaScript는 실행하지 않기 때문이죠.
리액트는 클라이언트 사이드에서 메타태그를 포함한 모든 컨텐트를 렌더링 합니다.
전체 웹앱을 감싸는 App Shell은 각 페이지마다 메타데이터를 심는 일은 쉽지 않습니다.
사이트맵은 웹사이트가 제공하는 페이지, 비디오 그리고 다른 여러 파일들간의 관계를 나타내는 파일입니다.
구글같은 서치엔진은 이 파일을 통해 효율적으로 웹사이트를 크롤링을 할 수 있어요.
리액트는 사이트맵 자동 생성기능을 빌트인으로 제공하지 않습니다.
만약 리액트 라우터로 라우팅 처리를 한다면, 시간과 노력이 좀 들더라도 라우팅 시 사이맵을 생성하는 툴을 이용해보세요.
SEO 최적화를 위해 셋업 시 고려해야 할 사항
robots.txt 파일을 최적화하세요.이제 SSR과 프리 렌더링이 가진 문제점을 다뤄보겠습니다.
'ismorphic'의 사전적 정의를 살펴보면 '동일한 구조의'라는 뜻을 가집니다.
리액트의 언어로 말해보자면, 서버와 클라이언트가 비슷한 형태로 되어있다는 뜻이겠죠.
즉, 동일한 리액트 컴포넌트를 서버와 클라이언트에서 재사용 할 수 있다는 말이 됩니다.
Isomorphic 방법론은 서버가 직접 사용자나 서치엔진에게 리액트앱을 렌더링 할 수 있고 그 말은 즉, JavaScript가 백그라운드에서 로드 및 실행되는 동안 컨텐츠를 즉시 보여줄 수 있다는 것입니다.
Next.js나 Gatsby같은 프레임워크는 Isomorphic 방법론을 대중화시켰습니다.
Isomorphic 방법론을 따르는 컴포넌트는 우리가 알던 기존 리액트 컴포넌트와는 조금 다르게 생겼습니다.
예를 들어, 클라이언트 대신 서버에서 구동되는 코드를 포함하는 것 처럼 말이에요.
심지어는 API secret까지 포함할 수도 있습니다. (물론 서버에서 클라이언트에게 전달하기 전에 완전히 제거합니다)
Next.js나 Gatsby같은 프레임워크는 적절한 추상화를 통해 복잡성을 어느정도 해소해주지만, 이는 정해진 방식대로 코드를 작성해야한다는 한계점이 존재한다는 뜻이기도 합니다.
서치엔진이 웹사이트를 랭크할 때 고려하는 요소들을 살펴보겠습니다.
쿼리의 속도와 정확성은 배제하고, 구글은 아래의 특성을 가진 웹사이트를 좋아합니다.
위의 특성들을 아래의 메트릭스로 정리해보자면,
동적 렌더링은 위의 각각의 메트릭스에 영향을 미칩니다.
우리는 리액트앱을 브라우저, 서버 그 밖의 여러가지 아웃풋에 렌더링 할 수 있습니다.
routing과 code splitting
이 두 가지 함수는 CSR과 SSR에서 극명한 차이를 보입니다.
CSR은 리액트 SPA의 기본 렌더링 경로입니다.
서버는 어떠한 컨텐츠도 담기지 않은 빈 Shell App을 전송합니다.
브라우저가 다운로드, 파싱, JavaScript를 포함한 소스코드 실행하면 그제서야 HTML 컨텐츠가 자리를 잡고 렌더링 됩니다.
(CSR + BootStrapped Data)
앞서 언급한 CSR과 동일한 상황이라고 가정해보겠습니다.
하지만 이번에는 DOM 렌더링 후 데이터 페칭이 이루어지는 것이 아니라, 서버가 HTML에연관 데이터를 부트스트래핑 한 뒤 보내준다면 아래와 같은 노드를 포함시킬 수 있습니다.
<script id="data" type="application/json">
{"title": "My blog title", "comments":["comment 1","comment 2"]}
</script>
그리고 컴포넌트가 마운트되면 파싱을 하게 됩니다.
var data = JSON.parse(document.getElementById('data').innerHTML);
서버로 왕복 할 필요가 없어졌지요.
(SSR to Static Content)
온라인 계산기를 만들고 있다고 가정해보겠습니다.
유저가 정렬을 위한 쿼리를 보내면 해당 쿼리를 실행해서 결과값을 계산하여 HTML 형태로 응답해줘야하는, 즉 HTML을 신속하게 생성해야하는 상황이라고 가정해보겠습니다.
이러한 상황에서 생성된 HTML은 간단한 구조로 이루어져있고, HTLM이 전송된 이후로는 DOM을 조종할 필요가 없습니다.
그저 HTML + CSS를 서비스 할 뿐이므로 renderToStaticMarkup메소드를 사용하면 됩니다.
라우팅은 서버에 의해 핸들링 됩니다. CDN 캐싱이 더 신속한 서버 응답을 보낼 수 있겠지만 각 결과에 따른 HTML을 서버가 재계산(recompute)해 줘야합니다.
(SSR with Rehydration)
앞서 서술한 상황에서, 클라이언트 사이드에서 동작하는 리액트가 필요하다고 가정해보겠습니다.
우리는 서버에서 첫번째 렌더링을 실행할 것이고, JavaScript 파일을 포함한 HTML 파일을 서버에서 보내주겠지요.
리액트는 서버에서 렌더링 된 마크업을 Rehydrate하는 과정을 거친 뒤에 이 어플리케이션은 CSR 어플리케이션처럼 실행됩니다.
리액트는 이를 수행하기 위한 빌트인 메소드를 제공합니다.
최초 요청은 서버가 핸들링하고 그 다음 부터의 렌더링들은 클라이언트 사이드에서 핸들링합니다. 클라이언트와 서버 모두에서 렌더링 되는 이런 앱들을 유니버셜 리액트 앱이라고 부릅니다.
Routing
라우팅은 클라이언트와 서버가 나누어 실행하거나 중복으로도 처리할 수 있습니다.
Code Splitting
ReactDOMServer는 React.lazy를 지원하지 않아 조금 까다로운 편입니다.
그래서 Loadable Components 같은 라이브러리를 사용해야 할 것 입니다.
또 알아두어야 할 점은 ReactDOMServer는 얕은 렌더링만 수행한다는 점이에요. 다시 말해, componentDidMount같은 라이프 사이클 메소드는 호출되지 않는다는 것입니다. 데이터를 불러오기 위해서는 다른 메소드를 사용하여 리팩터링이 필요할 것입니다.
Next.js같은 프레임워크가 등장하게 된 배경이지요.
SSRH의 까다로운 code splitting을 개선하고, 좀 더 매끄러운 개발 경험을 제공하도록 말입니다. 하지만 이러한 방법은 페이지 퍼포먼스 측면에서 봤을 때는 양날의 검이기도 해요.
(Pre-rendering to Static Content)
유저의 요청 전에 웹페이지를 미리 렌더링 할 수 있다면 어떨까요?
빌드 타임에 완성되거나 데이터가 동적으로 변경되었을때 말이에요.
Pre-rendering이란 유저가 요청을 보내기 전에 컨텐츠를 미리 렌더링 함을 의미합니다.
유저가 데이터에 미치는 영향이 비교적 미미한 블로그나 이커머스 앱에서 사용됩니다.
(Pre-rendering with Rehydration)
pre-rendering된 HTML을 클라이언트 사이드에서 렌더링 했을 때 완전한(fully-functional)리액트 앱으로써 동작하기를 바랄 때 사용합니다.
첫 번째 요청이 처리되면 앱은 스탠다드 리액트 앱처럼 동작할 것입니다.
routing과 code splitting 측면에서 SSRH와 유사합니다.
아래는 앞서 서술한 방법들이 웹 퍼포먼스에 미치는 영향과 그에 따른 점수를 매겨 비교한 표입니다.

PRS 방식은 가장 뛰어난 퍼포먼스를 수행합니다.
반면에 SSRH나 CSR방식은 기대 이하의 결과를 불러올 수 있습니다.
웹 사이트에 각기 다른 파트 별로 다수의 방법론을 적용하는 것도 가능합니다.
예를 들어, 개인정보 같은 데이터는 유저가 로그인 한 뒤에야 보일 수 있도록 위의 표를 참고해서 만들 수 있겠죠.
공개적으로 보여질 부분과 숨길 부분을 구분하여 적용할 수 있다는 뜻입니다.