만족

[React] 검색엔진 최적화(SEO):: Prerendering (react-hydratable) 본문

[React] 검색엔진 최적화(SEO):: Prerendering (react-hydratable)

FrontEnd/React Satisfaction 2022. 3. 8. 23:27

https://satisfactoryplace.tistory.com/131

 

[React] 검색엔진 최적화(SEO):: Prerendering (react-snap)

React는 대표적인 SPA제작 라이브러리이다. SPA가 사용감이나 개발적인 측면에서는 유리하지만, 빌드된 결과물을 보면 는 텅 비어있고 js가 body를 바꾸기 때문에 검색엔진은 이 사이트를 비어있는

satisfactoryplace.tistory.com

 

위 포스트에서 react-snap 으로 prerendering을 사용하는 방법에 대해 설명했었다.

 

그러나 언제부턴가 오류와 함께 동작하지 않게 되었고, 

해결 방법을 알아내기 위해 react-snap의 레포를 둘러보았으나 개발이 끊긴지 오래 되어

이슈는 많은데 픽스 버전이 전혀 나오지 않는 상태였다.

 

이리저리 뜯어 보다가 문득

어차피 복잡한 기능을 하는 모듈도 아닌데 직접 만들자 해서

회사도 일주일동안 쉬니 새로 각잡고 만들어봤다.

 

답답해서 직접 만들었다

 

이 모듈(react-hydratable)은 기본적으로 react-snap의 역할을 대체하기 위해 만들어졌다.

 

이 듣보잡모듈은 뭐지? 하고 나가지말고 좀만 읽어줘잉 ㅎ;

 

react-hydratable

https://github.com/NamGungGeon/react-hydratable

 

GitHub - NamGungGeon/react-hydratable: hold-on DOM structure for improving React SEO

hold-on DOM structure for improving React SEO. Contribute to NamGungGeon/react-hydratable development by creating an account on GitHub.

github.com

 

react 프로젝트의 index.js에서 render()또는 hydrate()를 사용해 react와 DOM을 연결한다.

 

이 때 render는 지정한 element(div#root)를 초기화시키고 그곳에 elements를 렌더링하고

hydrate는 html의 body가 채워져 있을 경우 그것을 채우는(수분화시키는) 작동을 한다.

 

물론 별도로 커스텀하지 않은 react 프로젝트(create-react-app으로 생성한 프로젝트 등)는

hydrate를 쓸 일이 없었을 것이지만, 내 프로젝트의 목표는 body가 채워진 html을 생성하는 것이였기 때문에

hydrate를 사용할 수 있게 만들어주는 react 모듈이라 해서 이름을 'react-hydratable'로 지었다.

 

설명은 위 링크에 능력 닿는 데까지 열심히 써 봤지만....

아무래도 영어실력이 구리다보니 이상한 표현이 있다면 누구라도 수정해주면 감사할 것 같다.

 

지금부터는 create-react-app으로 프로젝트를 생성하고,

react-hydratable을 사용해 body가 채워진 html을 만들어볼 것이다.

 

react-hydratable 사용해보기

npm run build

리액트 프로젝트를 빌드해보자

 

/build/index.html을 열어보면 아래와 같다.

 

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <link rel="manifest" href="/manifest.json" />
    <title>React App</title>
    <script defer="defer" src="/static/js/main.046127cb.js"></script>
    <link href="/static/css/main.073c9b0a.css" rel="stylesheet" />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

보이는 대로, body는 텅 비어있다.

 

물론 이걸 실행하면 script가 로드되고 실행되면서 body가 만들어진다만,

js를 실행할 능력이 없는 검색엔진은 이 사이트가 텅 빈 것으로 인식한다.

 

또 우리가 카카오톡으로 링크를 보내면 해당 사이트의 썸네일과 타이틀이 뜨는데,

이것들 역시 고정값에서 바뀌지 않는다.

 

react-hydratable을 사용하면 body가 채워진 html을 캡쳐할 수 있다.

 

npm install -D react-hydratable

 

react-hydratable을 설치한다.

 

그리고 캡쳐는 빌드가 완료된 다음에 이루어져야 하므로,

// package.json
{
  //...
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    
    "postbuild": "react-hydratable"
  },
  //...
}

package.json의 scripts에 postbuild 스크립트를 추가한다.

 

이렇게 구성하면 build가 완료된 후(postbuild) 

react-hydratable이 실행된다.

 

npm run build

다시 빌드해보자.

 

그러면 빌드 이후, 추가로 작업들이 실행되는 모습을 확인할 수 있다.

 

$ react-hydratable
react-hydratable: Config build...
react-hydratable: Config is here
 {
  webroot: '/Users/namgung-geon/react-hydratable/rhtest/build',
  host: 'http://localhost',
  port: 3000,
  crawlingUrls: [ '/' ],
  delay: 1500
}
createHttpServer: server is started
createHttpServer: ready (PORT 3000)
Crawling: start
Crawling: [Start]  http://localhost:3000/
Crawling: end
createHttpServer: server is closed
Crawling: [OK] http://localhost:3000/
✨  Done in 16.64s.

postbuild에 의해 지정된 명령인 react-hydratable이 실행되었다.

 

별 오류 없이 끝났다면 prerendering이 완료된 것이다.

 

정말로 잘 되었는지 /build/index.html을 확인해보자.

 

<head>
  <meta charset="utf-8" />
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <meta name="description" content="Web site created using create-react-app" />
  <link rel="apple-touch-icon" href="/logo192.png" />
  <link rel="manifest" href="/manifest.json" />
  <title>React App</title>
  <script defer="defer" src="/static/js/main.046127cb.js"></script>
  <link href="/static/css/main.073c9b0a.css" rel="stylesheet" />
</head>
<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"><div class="App">리액트좋아</div></div>
</body>

전과는 달리 div#root에 리액트좋아가 들어가 있다.

 

더 이상 body가 비어있지도 않고, script로드가 완료되면 기존 build와 다름없이 동작한다.

 

아직 해야할 일이 하나 남았다.

 

// /src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

/src/index.js를 보면 항상 render를 통해 ReactDOM을 그린다.

 

그러나 우리는 prerendering을 진행했기 때문에 div#root가 채워져 있고,

위처럼 항상 render로 화면을 그리게 되면,

화면이 로드되고 스크립트가 실행될 때 잠깐 화면이 깜빡이게 된다.

 

render는 div#root를 초기화하고 처음부터 렌더링하기 때문이다.

 

// /src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const element = (
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
const rootElement = document.getElementById('root');

if (rootElement.hasChildNodes()) {
  ReactDOM.hydrate(element, rootElement);
} else {
  ReactDOM.render(element, rootElement);
}

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

이렇게 변경하면 div#root가 비어있지 않은 경우는 body가 채워져 있으므로 hydrate로 이미 만들어진 element를 사용하게 하고,

그렇지 않은 경우는 react-hydratable로 prerendering되지 않은 페이지이므로 이전과 같이 동작한다.

 

npm run build

 

다시 빌드하면 완벽하게 동작한다.

 

여러 개의 페이지에 대해 prerendering하기

프로젝트 루트에 hydratable.config.json을 만든다.

 

// hydratable.config.js

{
  crawlingUrls: ['/', '/copyrights']
}

 

그리고 위와 같이 crawlingUrls에 prerendering을 원하는 경로를 배열로 전달하면

해당 페이지들을 모두 prerendering 할 수 있다.

 

더 많은 옵션은 아래 링크에서 확인하면 된다.

 

https://github.com/NamGungGeon/react-hydratable

 

GitHub - NamGungGeon/react-hydratable: hold-on DOM structure for improving React SEO

hold-on DOM structure for improving React SEO. Contribute to NamGungGeon/react-hydratable development by creating an account on GitHub.

github.com

 

결과물 미리보기

// package.json
{
  //...
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    
    "postbuild": "react-hydratable",
    
    "preview": "react-hydratable --preview"
  },
  //...
}

package.json에 preview 스크립트를 추가해주자.

 

이렇게 하면 기본적으로 http://localhost:3000 에서 build+prerendering된 결과물이 잘 동작하는지 확인할 수 있다.

 

조만간 react-hydratable을 typescript로 변환해 봐야겠다.



Comments