SEO 적용기 #하

react/ gatsby 환경에서 검색엔진 최적화를 적용한 내용을 정리하였습니다.


SEO 적용기는 총 2편의 시리즈로 구성되어 있습니다.

  1. SEO 적용기 #상
  2. SEO 적용기 #하
  3. SEO 적용기 #하 (현재글)


들어가며

이전 포스팅인 SEO 적용기 #상에서 아래 목록을 적용해보기로 하였습니다.

  • 탐색경로 목록
  • 사이트 맵
  • robot.txt
  • meta 태그

대부분의 설정과 작업을 현기 선임이 적용해주신 상태였기 때문에 전반적인 설정에 대해 학습하며 추가로 필요한 요소를 더했습니다. 학습한 내용에 대해서는 공식문서를 참고하는 형식으로 함께 포스팅하였습니다.

React Helmet Install

React Helmet은 React 컴포넌트에서 <head></head>를 제어할 수 있는 플러그인이므로 설치가 필요합니다.

npm install gatsby-plugin-react-helmet react-helmet

SEO Component

components 폴더에 seo.js를 만듭니다.

// components/seo.js

import * as React from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useStaticQuery, graphql } from 'gatsby';

const SEO = ({ lang, meta }) => {
  return (
    // Helmet: React Helmet 플러그인
    <Helmet
      htmlAttributes={{
        lang,
      }}
      meta={[].concat(meta)}
    />
  );
};

SEO.defaultProps = {
  lang: `en`,
  meta: [],
};

SEO.propTypes = {
  lang: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object),
};

Sitemap Install

Sitemap은 직접 설치/ Gatsby 플러그인 설치의 2가지 방법으로 설치할 수 있습니다. 플러그인으로 설치를 하면 글 배포 시 .xml 파일에 목록이 자동으로 추가되기 때문에 플러그인으로 설치를 하는 것을 추천합니다.

npm install gatsby-plugin-sitemap

sitemap-index.xml

gatsby 플러그인으로 설치를 하면 사이트맵인 .xml색인의 역할을 하는 index.xml 파일이 생성이 됩니다.
gatsby-plugin-sitemap Github 문서에 보면 ""entryLimit (숫자 = 45000) 사이트 맵 파일당 항목 수, 사이트 맵 색인 및 여러 사이트 맵은 항목이 더 많은 경우 생성됩니다."" 고 나옵니다. 즉 사이트맵의 항목이 많을 경우에만 생성된다는 것인데 조건에 맞지 않는 경우에도 색인이 생성되었습니다. 🤔

sitemap file


index.xml는 .xml을 가리키고 있으며 .xml은 포스팅 된 글의 URL 주소가 있습니다.

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>https://pxd-fed-blog.web.app/sitemap-0.xml</loc></sitemap>
</sitemapindex>

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
  <url>
    <loc>https://pxd-fed-blog.web.app/event/</loc>
    <changefreq>daily</changefreq>
    <priority>0.7</priority>
  </url>
  <url>
    <loc>https://pxd-fed-blog.web.app/chromeDebugging/</loc>
    <changefreq>daily</changefreq>
    <priority>0.7</priority>
  </url>
  <url>
    <loc>https://pxd-fed-blog.web.app/replaceRegExp/</loc>
    <changefreq>daily</changefreq>
    <priority>0.7</priority>
  </url>
  ...
</urlset>

robot.txt

robot.txt는 직접 설치/ Gatsby 플러그인 설치의 2가지 방법으로 설치할 수 있습니다.
블로그에 별다른 크롤러를 제어와 동적 생성이 불필요하기 때문에 직접 파일을 생성하였습니다.

Sitemap 주소는 sitemap-index.xml(색인) 파일로 지정하였습니다.

static/robot.txt

User-agent: *
Allow: /
Sitemap: https://pxd-fed-blog.web.app/sitemap-index.xml

Google Search Console 소유권 등록과 사이트맵 제출

Google에서 쉽게 블로그를 크롤링하고 분류할 수 있도록 하기 위해 Google Search Console에 소유권을 등록하고 사이트맵을 제출하였습니다.

Google Search Console 소유권 등록

search console01

- **URL 접두어**에 URL 주소 입력
search console02

- 다른 확인 방법에서 HTML 태그 선택 후 **메타태그 복사**
search console03

  • seo.js에 추가
// component/seo.js
return (
  <Helmet
    // ...
    meta={[
      {
        name: `google-site-verification`,
        content: `this is google site verification content`,
      },
      // ...
    ].concat(meta)}
  />
);

사이트맵 제출

  • Google Search Console sitemaps 선택

    search console04

  • 새 사이트맵 추가에 .xml 파일명 입력 후 제출 선택

    search console05
    search console06


meta 태그 설정

아래 속성으로 설정하였습니다.

// component/seo.js

const SEO = ({ meta }) => {
  return (
    <Helmet
      // ...
      meta={[
        {
          name: `google-site-verification`,
          content: `Q-BZs4XSsMP4CZzv3qnLQ9nwU1FCVuy3fdMUrbX0J2Q`,
        },
        {
          name: `robots`,
          content: `index, follow`,
        },
        {
          name: `description`,
          content: `PXD FED 그룹 블로그 입니다.`,
        },
        {
          property: `og:title`,
          content: `PXD FED GROUP BLOG`,
        },
        {
          property: `og:description`,
          content: `PXD FED 그룹 블로그 입니다.`,
        },
        {
          property: `og:type`,
          content: `website`,
        },
        {
          property: `og:image`,
          content: `https://pxd-fed-blog.web.app/logo_og.png`,
        },
      ].concat(meta)}
    />
  );
};

포스팅된 글에 따른 제목, 설명, 대표 이미지로 변경하기 위해 데이터를 가져오는 과정이 필요합니다.(react 작업에 대한 사전 지식이 필요한 부분이라 아래 공식문서를 먼저 확인해주세요 !)

기본정보 설정

gatsby-config.js내 siteMetadata에 블로그에 기본정보를 설정합니다.

// gatsby-config.js

module.exports = {
  siteMetadata: {
    title: `PXD FED GROUP`,
    author: {
      name: 'PXD FED GROUP',
      summary: '반갑습니다.',
    },
    description: `PXD FED 그룹 블로그 입니다.`,
    siteUrl: `https://pxd-fed-blog.web.app`,
    social: {
      recruit: 'http://pxd.co.kr/pages/jobs/jobs_form.html?recruit_num=56',
      github: 'https://github.com/pxd-fed/',
      gitlab: 'https://gitlab.com/pxd4group',
    },
    image: '/logo_og.png', // static 폴더에 위치
  },
};

기본정보 외 포스팅마다 제목, 설명, 대표 이미지를 변경해주기 위해 컴포넌트에 바인딩해 주는 코드를 추가합니다.
// pages/index.js (홈 component)

const BlogIndex = ({}) => {
  return (
    <Layout>
      <SEO title="PXD FED GROUP BLOG" />
    </Layout>
  );
};
// templates/blog-post.js (상세보기 component)

const BlogPostTemplate = ({ data }) => {
  const post = data.markdownRemark;
  const thumbnail =
    post.frontmatter.thumbnail.childImageSharp.gatsbyImageData.images.fallback.src;

  return (
    // 포스팅 제목, 설명, 썸네일
    <Layout>
      <SEO
        title={post.frontmatter.title}
        description={post.frontmatter.description || post.excerpt}
        thumbnail={thumbnail}
      />
    </Layout>
  );
};

BlogPostTemplate.propTypes = {};

export default BlogPostTemplate;

export const pageQuery = graphql`
  query BlogPostBySlug(
    $id: String!
  ) {
    markdownRemark(id: { eq: $id }) {
      id
      excerpt(pruneLength: 160)
      html
      frontmatter {
        title
        date(formatString: "MMMM DD, YYYY")
        description
        author
        thumbnail {
          childImageSharp {
            gatsbyImageData(placeholder: BLURRED, formats: [AUTO])
          }
        }
      }
    
  }
`;

meta 태그의 값 설정

// components/seo.js

import * as React from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useStaticQuery, graphql } from 'gatsby';

const SEO = ({ description, meta, title, thumbnail }) => {
  // useStaticQuery을 통해 siteMetadata GraphQL 쿼리를 받음
  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            title
            description
            social {
              recruit
              github
              gitlab
            }
            image
          }
        }
      }
    `,
  );

  // blog-post.js의 description/ thumbnail props 값 || siteMetadata의 description 값
  const metaDescription = description || site.siteMetadata.description;
  const metaThumbnail = thumbnail || site.siteMetadata.image;

  return (
    <Helmet
      meta={[
        // Search Console 소유권 태그
        {
          name: `google-site-verification`,
          content: `Q-BZs4XSsMP4CZzv3qnLQ9nwU1FCVuy3fdMUrbX0J2Q`,
        },
        {
          name: `robots`,
          content: `index, follow`,
        },
        {
          name: `description`,
          content: metaDescription,
        },
        // index.js와 blog-post.js의 title props 데이터
        {
          property: `og:title`,
          content: title,
        },
        {
          property: `og:description`,
          content: metaDescription,
        },
        {
          property: `og:type`,
          content: `website`,
        },
        {
          property: `og:image`,
          content: `https://pxd-fed-blog.web.app${metaThumbnail}`,
        },
      ].concat(meta)}
    />
  );
};

SEO.defaultProps = {
  meta: [],
  description: ``,
};

SEO.propTypes = {
  description: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object),
  title: PropTypes.string.isRequired,
  thumbnail: PropTypes.string,
};

export default SEO;

meta tag
메인 화면(상)/ SEO 적용기 #상(하) meta tag



탐색경로 목록

페이지 기준으로 Depth가 많지 않고, 메인 화면으로 이동하는 링크도 여럿 있기 때문에 새로운 탐색경로를 만들지 않고 이전/ 다음 글의 링크에 제목이 보일 수 있도록 정보를 추가하였습니다. 추후 썸네일까지 보일 수 있도록 수정할 예정입니다.

// templates/blog-post.js

const BlogPostTemplate = ({ data }) => {
  // ... 중략
  const { previous, next } = data;

  return (
    // 포스팅 제목, 설명, 썸네일
    <Layout>
      {/* 중략 */}
      <nav className="blog-post-nav">
        <ul
          style={{
            display: `flex`,
            flexWrap: `wrap`,
            justifyContent: `space-between`,
            listStyle: `none`,
            padding: 0,
          }}
        >
          <li className="post-nav-btn">
            {previous && (
              <Link to={previous.fields.slug} rel="prev">PREV {/* 제목 DOM 추가 */}
                <strong>{previous.frontmatter.title}</strong>
              </Link>
            )}
          </li>
          <li className="post-nav-btn">
            {next && (
              <Link to={next.fields.slug} rel="next">
                NEXT{/* 제목 DOM 추가 */}
                <strong>{next.frontmatter.title}</strong>
              </Link>
            )}
          </li>
        </ul>
      </nav>
    </Layout>
  );
};

BlogPostTemplate.propTypes = {};

export default BlogPostTemplate;

// previous, next 쿼리 추가
export const pageQuery = graphql`
  query BlogPostBySlug($previousPostId: String, $nextPostId: String) {
    previous: markdownRemark(id: { eq: $previousPostId }) {
      fields {
        slug
      }
      frontmatter {
        title
      }
    }
    next: markdownRemark(id: { eq: $nextPostId }) {
      fields {
        slug
      }
      frontmatter {
        title
      }
    }
  }
`;

반영 결과

result seo


추가적인 이슈

  1. Google Search console의 사이트맵 상태가 가져올 수 없음으로 유지
  2. 포스팅된 글을 검색할 경우 (예: pxd fed blog seo 검색)검색 결과가 홈 화면으로 나옴(바로가는 URL로 노출되지 않음)
  3. 구 도메인(github.io)이 검색 결과로 나오고 있음
  4. pxd fed blog을 검색할 경우에 검색 결과 첫 페이지에 pxd 관련 URL 노출(혹은 그 반대)

1~3번은 SEO 적용을 하면서 블로그의 도메인을 변경하였는데 그 이후로 크롤링이 꼬여버린 것 같습니다. 현기 선임이 파악한 원인 중의 하나는 크롤링 되는 요소(google analytics, gatsby-config.js 등)에 변경된 주소를 바로 반영하지 못했다는 점입니다. github 도메인이 검색결과에 계속 노출되는 것도 이 이유 때문이지 않을까 싶습니다.

4번은 크롤러가 그룹 블로그에서 pxd 홈페이지를 타고 다닐 수 있게 블로그에 링크 요소를 추가할 예정입니다. 현재는 pxd blog 검색했을 하면 첫번째 페이지의 그룹 블로그가 나오고 있습니다. (첫 페이지 개수에 설정에 따라서 두번째 페이지의 상위에 위치할 수도 있습니다.)

SEO 반영에 문제가 있을 경우 원인을 파악하는 것이 모호하고 만약 수정을 한다고 해도 반영되는 시간이 걸리는 특징이 있습니다. 따라서 시간을 두고 지속해서 살펴볼 예정입니다.

마치며

이번 포스팅은 학습과 적용 그리고 글을 정리하는데 어려웠습니다. SEO의 내용이 생각보다 방대했고 react를 잘 모르는 상태에서 설정에 관해 포스팅하다 보니 스스로 부족함을 느꼈습니다. 거기에 SEO 재적용할 경우 앞서 말씀드린 데로 원인 파악과 반영 시간 이슈 때문에 해결도 미미했습니다.

그렇지만 meta 태그 이외에 사이트맵과, robots.txt, search console 등 검색엔진 최적화에 모르고 있었던 부분에 대해 알아보고 기록화한 것은 의미가 있었습니다. 그리고 현기 선임이 모르거나 어려워 하는 부분을 많이 도와주셔서 포스팅할 수 있었습니다. 🙏

고맙습니다. - 끄읕 -