React SEO 적용

React SEO 적용

SPA(Single Page Application)에서 SEO(Search Engine Optimization)를 적용하는 방법

SPA에서 SEO를 적용하는 방법은 생각보다 까다롭습니다. CSR으로 자바스크립트로 html을 동적으로 렌더링 해서 페이지를 보여주는데, 크롤러 봇은 javascript 로 렌더링 하기 전의 html의 정보들을 가져옵니다.

그래서

  1. SSR 로 마이그레이션을 하던가,

  2. prerender 를 사용하여야 합니다.

    여기서 prerender란 페이지 요청을 낚아채어 사용자가 크롤러인지 여부를 확인하여 크롤러인 경우 캐시 된 버전의 페이지를 전달하여 줍니다. 반대로 크롤러가 아니라면 일반적인 페이지를 전달해주게 됩니다.

    netilify - prerendering 에 대한 설명

React-Helmet 으로 동적 적용

yarn add react-helmet-async

SEO를 적용할 컴포넌트를 만듭니다.

언어에 따라 title, description, 선택한 이미지에 따라 이미지가 바뀌게 고려를 해야했기 때문에 파라미터로 넘겨주었습니다.


import { Helmet } from 'react-helmet-async';



const SEO = ({
  pageUrl = '/',
  metaImage = `${process.env.PUBLIC_URL}main_img.jpg,
}: SEOProps): JSX.Element => {
  const { language } = useClientState();
  const title = 'garden.log';
  const description =
    'onethegarden blog';
  return (
    <Helmet
      title={title}
      htmlAttributes={{ lang: language }}
      meta={[
        // Google Meta Tags
        { itemProp: 'name', content: title },
        { itemProp: 'description', content: description },
        { itemProp: 'image', content: metaImage },
        // Facebook Meta Tags
        {
          property: 'og:url',
          content: `https://www.onethegarden.io${pageUrl}`,
        },
        {
          property: 'og:type',
          content: 'website',
        },
        //...이하 적용할 내용들
      ]}
    />
  );
};

export default SEO;

이렇게 적용을 해서 필요한 페이지마다 SEO 컴포넌트를 넣어줍니다.

const LoginPage = (): JSX.Element => {
  const location = useLocation();
  return (
    <>
      <SEO pageUrl={location.pathname} title={'login'} .../>
      <Header />
      <LoginTemplate>
        <Login />
      </LoginTemplate>
      <Footer />
    </>
  );
};

이렇게 적용하고 확인해보면 이런식으로 적용되어 있는 것을 볼 수 있습니다.

<meta property="og:type" content="website" data-rh="true">
<meta property="og:title" content="garden.log" data-rh="true">
<meta property="og:description" content="onethegarden blog" data-rh="true">
<meta property="og:image:height" content="256" data-rh="true">
<meta property="og:image:width" content="256" data-rh="true">
<meta property="og:image:height" content="256" data-rh="true">

... 

여기까지 해서 메타데이터를 미리 볼수 있는 사이트인 https://www.heymeta.com/ 에서 확인할 수 있습니다.

html에 적용한 메타데이터들만 읽히고 react-helmet으로 위에서 설정해준 정보들이 잡히지 않은 것을 볼 수 있습니다. (로컬에서 띄운 서버를 외부에서 접근하기 위해 ngrok을 사용하였습니다.)

React-Snap 적용

그래서 이제 react-snap을 적용해 줍니다.

yarn add react-snap

react-snap은 웹 앱을 정적 HTML로 미리 렌더링합니다. react-snap

먼저 빌드가 완료된 후 크롤링 할 수 있도록 package.json에 값을 추가해 줍니다.


  "scripts": {
    ...,
    "postbuild": "react-snap"
  },

Index.tsx 도 수정해줍니다.


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

if (rootElement?.hasChildNodes()) {
  hydrate(app, rootElement);
} else {
  render(app, rootElement);
}

hydrate는 렌더링은 하지 않고 이벤트 핸들러만 붙여줍니다. markup 이 있는 경우 렌더링 할 필요가 없기 때문입니다.

이 과정을 생략해도 되지만, 앱을 구동하는데 시간이 좀 더 걸린다고 합니다.

이제 빌드를 해보면 react-snap이 실행됩니다

npm run build 으로 빌드를 해주고

npx serve ./build 로 빌드된 내용을 로컬에서 띄워줍니다.

그리고 ngrok.io에서 띄운 링크를 HEY META 사이트에서 확인해보면 잘 적용이 된 것을 확인할 수 있습니다

하지만 이렇게 하면 메인 페이지에만 적용이 되기 때문에

Package.json에 적용할 페이지들을 명시해 줘야 합니다.

"scripts": {
  "postbuild": "react-snap"
},
  "reactSnap": { 
      "include": [ "/", "/blog", "/bloglist" ] 
   },
}