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

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

results matching ""

    No results matching ""