본문 바로가기

FRONTEND

[Next] 공식문서 읽어보기 Learn - SEARCH ENGINE OPTIMIZATION - Crawling and Indexing

출처: https://unsplash.com/ko/%EC%82%AC%EC%A7%84/O8CHmj0zgAg

해당 문서는 Next.js의 동작 원리를 심도있게 이해하고자 작성된 공식문서 번역 문서입니다. 원문의 의미를 최대한 반영하는 동시에 자연스럽게 이해할 수 있는 방향으로 번역하고자 노력하였습니다. 오역이 있을 수 있으니 확인 부탁드리며 피드백 주시면 반영하도록 하겠습니다.

원문주소: 
https://nextjs.org/learn/seo/crawling-and-indexing

 

크롤링과 인덱싱

검색 시스템과 구글 봇의 대략적인 작동 방식을 살펴보았으므로 크롤링과 인덱싱에 영향을 미치는 몇가지 핵심 요소에 대해 알아보도록 하겠습니다.

 

이번 챕터에서는 다음 주제를 살펴보도록 하겠습니다.

  • HTTP의 기본적인 상태 코드에 대해
  • 웹 컨턴츠 파싱 과정 중 웹 크롤러가 찾는 대상과 메타데이터에 대해
  • 구글과 소통하는 방법, 구글 검색 크롤러가 우리의 페이지에 새로운 컨텐츠가 생기는 시점을 어떻게 알게되는지
  • 검색엔진에게 당신이 바라는 인덱싱 상태를 알리기 위해 메타 로봇 태그와 캐노니컬 링크를 이용하는 방법

 

HTTP 상태 코드가 무엇일까?

HTTP 응답 상태 코드는 특정 HTTP 요청이 성공적으로 이루어졌는지 알려주는 역할을 합니다. 많은 종류의 상태 코드들이 있지만 검색엔진최적화(SEO) 관점에서 의미있는 상태는 일부 뿐입니다. 해당 상태 코드를 살펴보겠습니다.

 

200

🌏 성공 상태 응답 코드인 HTTP 200 OK 코드는 요청이 성공적으로 되었다는 것을 의미합니다.

웹 페이지를 구글에 인덱싱하기 위해서는 반드시 해당 페이지는 200 상태 코드를 반환해야 합니다. 이는 오가닉 트래픽을 받기 위해 일반적으로 원하는 상태 코드입니다. (오가닉 트래픽이란? 사용자가 검색 엔진에서 특정 키워드나 주제로 검색을 수행하여 나타난 검색 결과에서 클릭하여 웹 사이트로 방문하는 트래픽입니다.)

 

200 상태 코드는 Next.js가 페이지를 성공적으로 렌더링하는 경우 응답 받게되는 기본 코드입니다.

 

301/308

🌏 리다이렉트 상태 응답 코드인 HTTP 301 Moved Permanently는 요청한 리소스가 헤더에 주어진 URL 주소(destination URL)로 완전히 옮겨졌다는 것을 의미합니다.

이는 영구 리다이렉션 입니다. 일반적으로 가장 많이 사용되는 리다이렉션 타입입니다.

리다이렉션은 다양한 이유로 사용될 수 있지만, 핵심은 URL이 A 지점에서 B 지점으로 이동되었다는 것을 의미합니다.

리다이렉션는 페이지의 컨텐츠가 다른 지점으로 이동되는 경우 현재 및 잠재적인 고객을 잃지 않도록하고 봇이 지속적으로 해당 페이지를 인덱싱 하도록 보장하기 위해 필요합니다.

 

 🌏 참고: Next.js permanent redirects은 기본적으로 301 대신 308 상태 코드를 사용합니다. 308 상태 코드가 더 최신 버전이고 더 나은 옵션으로 간주됩니다.

 

두 상태 코드의 핵심적인 차이는 301은 요청 메서드를 POST 에서 GET으로 변경할 수 있지만 308은 그렇지 않다는 점입니다.

Next.js에서 getStaticProps 함수에서 props 대신 리다이렉션을 반환하는 것을 통해 308 리다이렉션을 트리거 할 수 있습니다.

 

//  pages/about.js
export async function getStaticProps(context) {
  return {
    redirect: {
      destination: '/',
      permanent: true, // triggers 308
    },
  };
}

 

또한 next.config.js 파일에서 permanent: true 키를 사용할 수 있습니다.

 

//next.config.js

module.exports = {
  async redirects() {
    return [
      {
        source: '/about',
        destination: '/',
        permanent: true, // triggers 308
      },
    ];
  },
};

 

302

리다이렉션 상태 응답 코드인 HTTP 302 Found은 요청한 리소스가 일시적으로 헤더에 지정된 URL로 일시적으로 이동되었음을 나타냅니다.

대부분의 경우에서는 302 리다이렉션은 301 리다이렉션으로 대체되어야 합니다. 사용자를 일시적으로 특정 페이지(예: 프로모션 페이지)로 리다이렉션 시키거나 위치에 따라 사용자를 리다이렉션 시키는 경우에는 해당되지 않을 수 있습니다.

 

404

HTTP 404 Not Found 클라이언트 에러 응답 코드는 서버가 해당 요청 리소스를 찾을 수 없다는 것을 나타냅니다.

 

위에서 언급한대로, 이동된 페이지는 HTTP 301 상태 코드를 사용하여 새로운 위치로 리다이렉션 되어야 합니다. 만약 리다이렉션이 되지 않는다면 URL은 404 상태 코드를 반환할 것입니다.

 

404 상태 코드는 그 자체로 나쁜 것 만은 아닙니다. 사용자가 존재하지 않는 URL에 방문하게 된다면 404 상태 코드를 응답하는 것이 원하는 결과이지만 이는 검색 순위가 기대에 미치지 못하는 결과를 초래하기 때문에 이와 같은 상황이 빈번하게 발생하지 않도록 해야합니다.

 

Next.js는 어플리케이션에 존재하지 않는 URL에 대해서 자동으로 404 상태 코드를 반환합니다. 어떤 경우 페이지에서 404 상태 코드를 반환하고 싶을 수 있습니다. 이를 위해 props를 사용하는 것 대신 다음의 코드를 사용해 이를 달성할 수 있습니다.

 

export async function getStaticProps(context) {
  return {
    notFound: true, // triggers 404
  };
}

 

pages/404.js 를 생성을 통해 빌드 타임에 정적으로 생성되는 커스텀 404 페이지를 만들 수 있습니다.

예를들어 다음과 같이 만들 수 있습니다.

// pages/404.js
export default function Custom404() {
  return <h1>404 - Page Not Found</h1>;
}

 

410

HTTP 410 Gone 클라이언트 에러 응답 코드는 원본 서버에서 대상 리소스에 대해 더 이상 접근할 수 없으며 이 상태가 영구적일 가능성이 있음을 의미합니다.

 

이 상태 코드는 자주 사용되지는 않지만 더 이상 존재하지 않게 될 웹사이트의 컨텐츠를 삭제하는 경우 이 상태 코드를 찾아보는 것이 좋습니다.

 

HTTP 410 Gone 상태 코드가 적절하게 사용 되는 경우는 다음과 같습니다:

  • e-커머스: 장바구니에서 제품을 영구적으로 제거
  • 포럼: 사용자에 의해 제거된 스레드
  • 블로그: 사이트에서 제거된 블로그 글

이 상태 코드는 크롤러 봇에게 해당 컨텐츠를 다시 방문하지 않도록 함에 대해 알려줍니다.

 

500

HTTP 500 Internal Server Error 서버 에러 응답 코드는 요청을 처리하는 과정에서 서버가 예상하지 못한 상황에 놓였다는 것을 나타냅니다.

 

Next.js는 예상치 못한 어플리케이션 에러에 대해 자동적으로 500 상태 코드를 반환합니다. 여러분은 500 에러 커스텀 페이지를 만들 수 있습니다. 해당 페이지는 pages/500.js를 만드는 것을 통해 빌드타임에 정적으로 생성됩니다.

 

예를 들면 다음과 같습니다.

// pages/500.js
export default function Custom500() {
  return <h1>500 - Server-side error occurred</h1>;
}

 

HTTP 503 Service Unavailable 서버 에러 응답 코드는 서버가 요청을 처리할 준비가 되지 않은 것을 의미합니다. 여러분의 사이트가 다운되거나 오랜 시간 동안 다운이 될 것이라 예상이 되는 경우에 해당 상태 코드를 반환하는 것이 권장됩니다. 이를 통해 장기적으로 순위가 하락하는 것을 방지할 수 있습니다.

 

robots.txt 파일은 무엇일까?

robots.txt 파일은 검색 엔진 크롤러에게 해당 사이트에서 크롤러가 요청할 수 있는 파일이나 페이지에 대해 알려줍니다. robots.txt 파일은 특정 도메인에서 파일을 요청하기전 대부분의 성능이 좋은 봇들이 참조하는 웹 표준입니다.

 

웹사이트에서 특정 부분을 크롤링 되고 인덱싱 되는 것을 보호하고 싶을 수 있습니다. 예를 들면, CMS 또는 관리자페이지(admin), e-커머스 사용자 계정 정보, 일부 API 라우트 등이 있습니다.

 

이 파일들은 각각 호스트의 루트에서 제공되어야만 하거나 대안으로 루트 경로인 /robots.txt를 대상 URL로 리다이렉션 시킬 수 있으며 대부분의 봇들이 해당 리다이렉션을 따를것입니다.

 

Next.js 프로젝트에 robots.txt file 추가하는 법

Next.js의 정적 파일을 제공 기능 덕분에 쉽게 robots.txt 파일을 추가할 수 있습니다. 루트 디렉토리에 public 폴더dp robots.txt 라는 새 파일을 생성하면 됩니다.

 

robots.txt 파일에 다음의 내용 등을 작성할 수 있습니다.

//robots.txt

# Block all crawlers for /accounts
User-agent: *
Disallow: /accounts

# Allow all crawlers
User-agent: *
Allow: /

 

yarn dev 명령어를 통해 어플리케이션을 실행하면,  http://localhost:3000/robots.txt에서 이용가능합니다. public 폴더의 이름은 URL 주소의 일부가 아닌 것을 주의해야 합니다.

 

public 폴더의 이름을 다른 것으로 변경하지 마세요. 해당 이름은 변경할 수 없고 정적 자료를 제공하는 유일한 디렉토리 입니다.

 

XML 사이트맵

사이트맵은 구글과 커뮤니케이션할 수 있는 가장 쉬운 방법입니다. 사이트맵은 웹사이트에 속해있는 URL과 해당 URL이 업데이트된 시기를 알려주어 구글이 웹사이트를 좀 더 효율적으로 크롤링하고 새로운 컨텐츠에 대해 감지할 수 있도록 합니다.

 

XML 사이트맵이 가장 널리 알려져있고 가장 많이 사용되지만, RSSAtom을 통해서도 생성할 수 있고 심지어 최대한 간단한 방식을 선호하는 경우 Text 파일을 통해 생성할 수 있습니다.

 

사이트맵은 사이트에 대한 페이지, 비디오, 기타 파일들과 에 대한 정보와 이들 간의 관계를 제공하는 파일입니다. 구글과 같은 검색 엔진은 해당 파일을 읽어 좀 더 현명한 방식으로 사이트를 크롤링 합니다.

 

구글에 따르면, 다음과 같은 상황에서 사이트맵이 필요할 수 있습니다:

  • 사이트가 매우 큰 경우. 결과적으로, 구글 웹 크롤러가 새로운 페이지나 최근 업데이트된 페이지 중 일부를 놓칠 가능성이 있습니다.
  • 서로 잘 연결되어 있지 않거나 고립되어 있는 페이지가 많은 경우. 사이트 페이지가 서로 자연스럽게 상호 참조하지 않는 다면, 사이트맵에 해당 페이지들을 나열해서 구글이 페이지의 일부를 놓치지 않을 수 있도록 할 수 있습니다.
  • 웹사이트가 새롭게 작성되었거나 외부 링크에 연결된 경우. 구글봇과 다른 웹 크롤러는 한 페이지에서 다른 페이지로 링크를 따라 이동하며 웹페이지를 이동합니다. 그렇게 때문에 링크로 연결되어 있지 않다면 구글이 해당 페이지를 발견하지 못할 것입니다.
  • 비디오 또는 이미지를 포함한 풍부한 미디어 컨텐츠가 있거나 구글 뉴스에 노출되는 경우. 제공되는 경우에 구글은 사이트맵을 통해 얻게되는 추가적인 정보를 검색에 반영할 수 있습니다.

사이트맵은 탁월한 검색 엔진 성능에 필수적인 요소는 아니지만, 봇의 크롤링, 인덱싱 기능을 촉진하고 이를 통해 사이트의 컨텐츠가 더 빠르게 수집되고 그에 따라 순위가 결정 될 것입니다.

 

사이트맵을 사용하는 것과 새로운 컨텐츠가 웹사이트 전반에 걸쳐 생성될 때에 사이트맵을 동적으로 만드는 것은 매우 권장되는 사항입니다. 정적 사이트맵도 사용가능하지만 구글에게 있어 지속적인 검색 목적으로는 그다지 유용하지 않습니다.

 

Next.js 프로젝트에 사이트맵 추가하기

두 가지 방법을 통해 추가할 수 있습니다:

 

  • 수동

상대적으로 간단하고 정적 사이트의 경우에는 해당 프로젝트 public 디렉토리에sitemap.xml파일을 만들 수 있습니다.

  <!-- public/sitemap.xml -->
   <xml version="1.0" encoding="UTF-8"><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>http://www.example.com/foo</loc><lastmod>2021-06-01</lastmod></url></urlset></xml>

 

  • getServerSideProps

웹사이트는 동적일 가능성이 높습니다. 이런 경우에 필요에 따라 즉시 XML 사이트맵을 생성하기 위해 `getServerSideProps` 을 이용할 수 있습니다. 

pages 디렉토리에 `pages/sitemap.xml.js` 같은 형태로 새로운 페이지를 만들 수 있습니다. 해당 페이지의 목적은 data를 요청하는 API를 호출하여 동적 페이지에 대한 URL을 알 수 있도록 하는 것입니다. 그런 다음 XML 파일을 작성하여 `/sitemap.xml` 에 대한 응답으로 제공합니다.

//pages/sitemap.xml.js
const EXTERNAL_DATA_URL = 'https://jsonplaceholder.typicode.com/posts';

function generateSiteMap(posts) {
  return `<?xml version="1.0" encoding="UTF-8"?>
   <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
     <!--We manually set the two URLs we know already-->
     <url>
       <loc>https://jsonplaceholder.typicode.com</loc>
     </url>
     <url>
       <loc>https://jsonplaceholder.typicode.com/guide</loc>
     </url>
     ${posts
       .map(({ id }) => {
         return `
       <url>
           <loc>${`${EXTERNAL_DATA_URL}/${id}`}</loc>
       </url>
     `;
       })
       .join('')}
   </urlset>
 `;
}

function SiteMap() {
  // getServerSideProps will do the heavy lifting
}

export async function getServerSideProps({ res }) {
  // We make an API call to gather the URLs for our site
  const request = await fetch(EXTERNAL_DATA_URL);
  const posts = await request.json();

  // We generate the XML sitemap with the posts data
  const sitemap = generateSiteMap(posts);

  res.setHeader('Content-Type', 'text/xml');
  // we send the XML to the browser
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
}

export default SiteMap;

 

검색엔진을 위한 특별 메타 태그

메타 로봇 태그는 검색 엔진이 항상 따르는 지시문 입니다. 이 로봇 태그를 추가하는 것은 사이트에 대한 색인화를 좀 더 쉽게합니다.

 

지시문과 제안은 차이가 있습니다. 메타 로봇 태그 또는 robots.txt 파일은 지시문이며 항상 준수해야 합니다. 반면, 캐노니컬 태그는 구글이 준수할지 말지에 대한 여부를 결정하는 권고사항입니다.

 

페이지 단위 메타 태그에 대한 선택지는 많이 있지만 다음은 검색엔진최적화(SEO)와 흔히 사용 되는 예시입니다:

<meta name="robots" content="noindex,nofollow" />

 

로봇 태그는 가장 흔하게 볼 수 있는 태그 입니다. 기본적으로 index,follow 로 값을 갖기 때문에 명시해줄 필요가 없으며 all 또한 유효한 대체 버전입니다.

<meta name="robots" content="all" />

 

위의 예시에서 처럼 로봇 태그를 noindex,nofollow 로 설정하는 것을 통해 검색 엔진에게 다음의 내용을 알려줍니다.

 

  • noindex

검색 결과에 해당 페이지가 나타나지 않게 하기 위해 사용합니다. noindex 생략하면 해당 페이지가 검색 결과에 나타나고 색인화 됩니다.

웹사이트를 구축할 때, 특정 페이지에 대한 색인화를 원치 않을 수 있습니다. 보통 설정페이지, 내부 검색 페이지, 정잭 등에 대한 페이지를 포함합니다.

 

  • nofollow

해당 페이지의 링크를 따라가지 않게 하기 위해 사용합니다. nofollow를 생략하면 해당 페이지에 대해 로봇이 링크를 크롤링하고 따르는 것을 허락합니다. 다른 페이지의 링크는 크롤링을 활성화 시킬 수 있기 때문에 예를 들어, 링크 A가 X , Y 페이지에 나타나고 X 페이지가 nofollow 로봇 태그를 가지고 있고 Y 페이지에는 없는 경우, 구글은 해당 링크에 대한 크롤링 여부를 결정할 수 있습니다.

 

참고: 구글 공식 문서에서 전체 지시문 목록을 확인할 수 있습니다.

 

 

Googlebot 태그

<meta name="googlebot" content="noindex,nofollow" />

가끔 googlebot 태그를 볼 수도 있습니다. 대부분의 경우 robots 태그만으로 충분합니다. googlebot 태그는 구글에만 특정된 태그입니다. 구글봇에 대한 예외 규칙을 적용하고 일반적인 규칙을 그 외의 검색 엔진에 적용하기를 원하는 경우 해당 태그를 사용하세요.

 

태그가 충돌하는 경우 더 엄격한 태그가 적용됩니다.

 

크롤링 되기를 원하지 않는 URL을 robots.txt 에 추가할 수 있음에도 이러한 태그가 필요한 이유에 대해 궁금할 수 있습니다. 메타 태그는 필요에 따라 페이지를 noindex 로 표기할 수 있는 유연성을 부여합니다.

 

예를들어, 상품 페이지에 필터를 적용했을 때 나오는 결과가 없다면, 해당 페이지를 noindex 로 표기하는 것이 일반적입니다.

 

robots.txt 파일을 통해 봇의 크롤링을 방지한 URL은 구글에 의해서 절대로 크롤링 되지 않을 겠지만, 페이지가 색인화된 이후 해당 규칙이 추가된 경우에는 해당 페이지는 색인화 된 상태로 남아있을 수 있습니다. 해당 페이지가 색인화 되지 않도록 하는 가장 확실한 방법은 noindex 태그를 사용하는 것입니다.

 

참고: 구글은 페이지를 크롤링하지 않고 페이지 색인화 하는 것을 결정할 수 있습니다. 극히 드문 경우이긴 하지만, 구글이 사용자 기대 사항이 페이지에 꼭 포함되도록 하는 것과 어떤 페이지가 특정 검색 결과를 만족 시키길 원하는 경우 그렇게 하기도 합니다.

 

Google 태그

nositelinkssearchbox

<meta name="google" content="nositelinkssearchbox" />

사용자가 사이트를 검색할 때, 구글 검색 결과는 때때로 해당 사이트에 대한 검색창와 해당 사이트로 연결된 다이렉트 링크를 보여주기도 합니다. 해당 태그는 구글에게 사이트 링크 검색창을 표시하지 않도록 알려줍니다.

 

notranslate

구글은 사용자가 사용자가 일곡자 하는 언어와 일치하지 않는 사이트 컨텐츠를 인지할 때, 검색 결과에 대한 번역 링크를 제공합니다.

 

일반적으로, 이는 훨씬 더 큰 규모의 사용자들에게 독특하고 매력적인 컨텐츠를 제공할 수 있는 기회가 될 수 있습니다. 하지만 이것이 바람직하지 않은 상황이 있을 수 있습니다. 이 메타 태그는 구글에게 해당 페이지에 대한 번역을 제공하지 않기를 원한다고 알려줍니다.

 

Example

우리가 마주칠 수 있는 흔히 사용되는 태그를 살펴보았으므로 해당 일부 태그들에 대한 실사용 예시를 살펴보겠습니다.

 

import Head from 'next/head';

function IndexPage() {
  return (
    <div>
      <Head>
        <title>Meta Tag Example</title>
        <meta name="google" content="nositelinkssearchbox" key="sitelinks" />
        <meta name="google" content="notranslate" key="notranslate" />
      </Head>
      <p>Here we show some meta tags off!</p>
    </div>);
}

export default IndexPage;

 

위의 예시에서 볼 수 있듯이, 페이지의 head에 요소를 추가하기 위한 내장된 next/head 컴포넌트를 사용하고 있습니다.

 

head 태그 내에 중복된 태그를 피하기 위해서 key 프로퍼티를 사용할 수 있습니다. 이를 통해 태그가 한 번만 렌더링 되도록 할 수 있습니다.

 

 

캐노니컬 태그는 무엇일까?

캐노니컬 URL은 검색 엔진이 생각하기에 사이트의 중복 페이지 모음 중 가장 대표적인 페이지에 대한 URL 주소를 가리킵니다.

 

직접적으로 검색 엔진에게 캐노니컬 URL에 대해 알릴 수 있지만, 알리는 것 없이 검색 엔진이 여러 URL을 그룹화하는 것을 결정할 수도 있습니다. 구글이 여러 다른 경로를 통해 URL을 찾을 수 있다면 이를 자동으로 처리할 수 있습니다.

 

구글이 이를 감지하는데 많은 일들을 행하지만, 그 시스템을 대규모의 형태로 동작하고 모든 예외 케이스를 다루지 않습니다. 캐노니컬 태그는 웹사이트가 좋은 성능을 보장하기 위해 다루어야할 중요한 요소 입니다.

 

만약 구글이 동일한 컨텐츠를 가지고 있는 많은 URL을 발견한다면, 해당 URL을 중복된 것으로 간주하여 검색 결과에서 뒤로 미룰 수도 있습니다.

 

이는 또한 도메인 전반에 걸쳐 발생하며, 만약 두 개의 다른 웹사이트를 운영하고 각 사이트에 동일한 컨텐츠를 게시한다면, 검색엔진은 검색 순위에 랭크할 하나의 컨텐츠를 결정할 수 있고 직접적으로 두 경우 모두 미룰 수 있습니다.

 

이 지점에서 캐노니컬 태그가 유용해집니다. 캐노니컬 태그는 구글에게 어떤 URL이 원본인지 중복된 것인지에 대해 알려줍니다. 같거나 다른 도메인의 중복된 많은 페이지들은 검색 순위 저하나 심지어는 패널티를 받기도 합니다.

 

e-커머스 쇼핑몰의 상품을 example.com/products/phone and example.com/phone 두 도메인을 통해 접근할 수 있다고 가정해 봅시다.

 

두 경우 유효하고 동작하는 URL 이지만, 존재하는 중복 컨텐츠 감지를 막기 위해 캐노니컬 태그를 사용합니다.

 

<link rel="canonical" href="https://example.com/products/phone" />

 

캐노니컬 태그는 검색 엔진 최적화 성능에 있어 필수적인 요소입니다. 그 이유는 캐노니컬 태그를 통해 서로 다른 URL을 만들 수 있고 사용자나 마케팅 도구를 통해 이를 생성할 수도 있기 때문입니다.

 

구글에서 몇가지 마케팅 캠페인을 진행중이고 구글이 UTM 파라미터를 추가하기로 결정한다고 가정해 봅시다. 이 새롭고 고유한 URL이 구글봇에 의해 색인화될 것이므로 중복된 페이지를 통일하기 위해 여전히 캐노티컬 태그를 표시해야 합니다.

 

예시

import Head from 'next/head';

function IndexPage() {
  return (
    <div>
      <Head>
        <title>Canonical Tag Example</title>
        <link
          rel="canonical"href="https://example.com/blog/original-post"key="canonical"/>
      </Head>
      <p>This post exists on two URLs.</p>
    </div>);
}

export default IndexPage;