3-4. React-Helmet 을 통한 페이지 head 태그 작성하기
페이지의 제목과, 구글 등의 검색엔진이 페이지를 수집 할 때 페이지의 기본 정보로 사용 할 description 메타태그를 설정하려면 페이지의 head 태그를 작성해야 합니다.
리액트를 사용하여 클라이언트 라우팅을 사용하는 프로젝트의 경우엔, 자바스크립트를 사용하여 직접 처리를 해주어야 합니다. 페이지 제목는 document.title 값을 수정하여 반영을 해 줄 수 있어서 비교적 간단한 편이지만, meta 태그의 경우에는 다음과 같은 형식으로 직접 주입을 해줘야만 합니다:
const meta = document.createElement('meta');
meta.name = "description";
meta.content = "페이지 정보";
document.getElementsByTagName('head')[0].appendChild(meta);
태그의 종류가 많아지면 꽤 복잡하겠지요? react-helmet 라이브러리를 사용하게 된다면, 이러한 작업을 JSX 를 사용하여 직접 태그를 작성하듯이 페이지의 head 를 설정 할 수있습니다.
설치와 적용
이 라이브러리를 yarn 을 통해 설치하세요.
$ yarn add react-helmet
이 라이브러리를 사용하면 다음과 같은 형식으로 페이지의 head 정보를 입력 할 수 있습니다.
import React from "react";
import {Helmet} from "react-helmet";
class Application extends React.Component {
render () {
return (
<div className="application">
<Helmet>
<meta charSet="utf-8" />
<title>My Title</title>
<link rel="canonical" href="http://mysite.com/example" />
</Helmet>
...
</div>
);
}
};
어떤가요? 편리하지요? 일반 HTML 을 작성 할 때 처럼 XML 형태로 작성할 수 있게 됩니다. 한가지 주의 하실 점은, JSX 이기 때문에 꼭 태그를 닫아주어야 합니다. (반면 HTML 에서는 태그를 닫지않아도 정상적으로 작동합니다.)
Helmet 은 컴포넌트 형식으로 작성을 하게 되는데요, 만약에 동일한 종류의 정보가 다른곳에서 설정이 된다면 (예: App 에서 title 설정을 하고, 또 Post 에서 title 을 설정 한 경우) 더욱 깊숙한 DOM 에서 렌더링 된 것이 우선권을 가지게 됩니다.

자, 그럼 우리 프로젝트에 Helmet 을 적용해보겠습니다. 앞으로 Helmet 을 적용할 컴포넌트는 두개입니다: Post, ListPage
먼저 Post 부터 적용을 해봅시다.
src/containers/post/Post.js
(...)
import removeMd from 'remove-markdown';
import { Helmet } from 'react-helmet';
class Post extends Component {
(...)
render() {
const { loading, post } = this.props;
if(loading) return null; // 로딩중일땐 아무것도 보여주지 않음
const { title, body, publishedDate, tags } = post.toJS();
return (
<div>
{ /* body 값이 있을 때만 Helmet 설정 */ body && (
<Helmet>
<title>{title}</title>
<meta name="description" content={removeMd(body).slice(0, 200)}/>
</Helmet>)
}
<PostInfo title={title} publishedDate={publishedDate} tags={tags}/>
<PostBody body={body}/>
</div>
)
}
}
(...)
포스트의 제목과 내용을 head 정보에 넣도록 코드를 작성했습니다. 메타 태그의 description 은 너무 길으면 안되므로, 200자로 제한을 설정했습니다. 이 과정에서, body 값이 null 이라면 오류가 발생하므로 Helmet 정보가 body 가 존재 할 때만 렌더링 하도록 설정하였습니다.

포스트 페이지를 열어서 페이지 제목이 잘 바뀌었는지 확인해보세요.
다음 작업은 ListPage 컴포넌트입니다. 여기서는 현재 몇 번째 페이지를 보고있는지, 그리고 태그를 설정했다면 어떤 태그를 보고있는지도 타이틀에 입력해보겠습니다.
src/pages/ListPage.js
(...)
import { Helmet } from 'react-helmet';
const ListPage = ({match}) => {
// page 의 기본값을 1로 설정합니다.
const { page = 1, tag } = match.params;
// title 값을 page 와 tag 값에 따라 동적으로 설정합니다
const title = (() => {
let title = 'reactblog';
if (tag) {
title += ` #${tag}`
}
if(page !== 1) {
title += ` - ${page}`;
}
return title;
})();
return (
<PageTemplate>
<Helmet>
<title>{title}</title>
</Helmet>
코드를 작성하고 나면, 포스트 목록 페이지에서도 제목이 보여지게 됩니다.

서버사이드 렌더링에서 적용
현재 head 태그들이 클라이언트에서만 작동하고있으므로, 서버에서도 해주어야 검색엔진이 정보를 제대로 수집 할 수 있습니다. Helmet 에는 서버 사이드 렌더링을 할 때 사용 할 수 있는 함수 renderStatic 이라는 함수가 있습니다. 한번 사용해볼까요?
src/ssr.js
(...)
import { Helmet } from 'react-helmet';
const render = async (ctx) => {
(...)
// renderToString 은 렌더링된 결과물을 문자열로 만들어줍니다.
// 서버에서는 BrowserRouter 대신에 StaticRouter 를 사용합니다.
const html = ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter location={url} context={context}>
<App/>
</StaticRouter>
</Provider>
);
// isNotFound 값이 true 라면
if(context.isNotFound) {
ctx.status = 404; // HTTP 상태를 404로 설정해줍니다
}
const helmet = Helmet.renderStatic();
const preloadedState = JSON.stringify(transit.toJSON(store.getState()))
.replace(/</g, '\\u003c');
return { html, preloadedState, helmet };
}
export default render;
renderStatic 은 한번 렌더링 작업이 완료 된 다음에 실행되어야 합니다. 이 함수를 통하여 얻게 된 객체를 함수의 반환값에 넣어주었습니다. Helmet 사용과 서버사이드 렌더링을 함께 하는 경우, 이 작업은 필수적입니다. 만약에 이 작업을 생략하게 된다면 메모리 누수 현상이 발생하게 됩니다.
renderStatic 을 사용하여 만들어진 객체는 다음 값들을 지니고 있습니다: base, bodyAttributes, htmlAttributes, link, meta, noscript, script, style, title
해당 항목들에 .toString() 을 실행하면 문자열로 변환되고, toComponent() 를 실행하면 리액트 컴포넌트가 만들어집니다.
자, 준비가 다 되었으니 서버쪽에서 이 값들을 받아서 html 에 넣어봅시다.
서버쪽의 서버사이드 렌더링 관련 코드에서, buildHtml 부분을 다음과 같이 수정하세요.
src/ssr/index.js
function buildHtml({ html, helmet, preloadedState }) {
const { title, meta } = helmet;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="/manifest.json">
<link rel="shortcut icon" href="/favicon.ico">
${title.toString()}
${meta.toString()}
<link href="/${manifest['app.css']}" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">${html}</div>
<script>
window.__PRELOADED_STATE__ = ${preloadedState}
</script>
<script type="text/javascript" src="/${manifest['vendor.js']}"></script>
<script type="text/javascript" src="/${manifest['app.js']}"></script>
</body>
</html>`;
이제 모든 작업이 완료되었습니다! 프로젝트를 다시 빌드하고 localhost:4000 으로 접속하여 head 정보가 제대로 나타나는지 확인해보세요.
$ yarn build
$ yarn build:server

잘 나타났나요? 축하합니다! 여러분은 이 블로그 프로젝트를 완성하셨습니다!