본문 바로가기

AWS

Lambda@Edge를 활용하여 SPA에서 Dynamic OG Tag, SEO 적용하기

SPA(Single Page Application)에서 SEO(Search Engine Optimization)의 문제점

SPA에서는 일반적으로 root 디렉터리의 index.html 하나의 단일 페이지로 구성되어있기 때문에 하위 디렉터리에 대한 SEO가 불가합니다. 구글은 자바스크립트까지 분석하여 어느 정도 하위 디렉터리도 분석하여 SEO가 가능하다고 합니다.

 

Dynamic OG(Open Graph protocol) Tag 적용하기

위에서 설명한 SEO 외에 페이지의 특정 상품에 대한 상세페이지와 같이 페이지마다 각각 다른 Open Graph 정보를 삽입하기 위해서는 추가 작업이 필요합니다.

  1. SSR(Server Side Rendering)으로 페이지 구성을 변경
  2. Prerender.io 서비스를 이용하기
  3. 서버에서 동적 렌더링을 직접 구현
  4. AWS Lambda@Edge 이용하기

1. 이미 만들어진 CSR(Client Side Rendering) 페이지를 SSR로 변경하는 방법은 프로젝트 초기가 아니라면 쉽지 않은 선택입니다. SSR을 이용하여 구현해보지 않았다면 추천하지 않습니다.

2. 가장 쉬운 방법이지만 비용이 꽤 들어가고 원하는 대로 커스텀이 불가할 수 있습니다.

3. developers.google.com/search/docs/guides/dynamic-rendering 를 참고하여 동적 렌더링을 수행할 서버를 구현하면 되는데 서버 관련 지식이 없다면 구현하기 쉽지 않습니다.

4. 이 방법은 전제조건으로 AWS의 Cloudfront를 사용하고 있어야 합니다. Lambda@Edge를 Cloudfront와 연동하여 사용하기 때문입니다. 비용적인 측면은 굉장히 저렴한 편입니다. serverless 방식으로 동작하기 때문에 추가적으로 서버를 구축할 필요도 없습니다.

 

이러한 이유로 이번에는 Lambda@Edge를 활용하여 Dynamic OG Tag를 적용해 보겠습니다.

 

Lambda@Edge란 무엇인가?

보통 SPA로 만든 웹사이트를 구축할 때 빌드한 페이지를 AWS S3에 올리고 Cloudfront를 이용하여 도메인을 연결하고 유저의 요청이 오면 S3에 올라간 파일을 찾아서 제공합니다.

이때 유저의 요청에 따라 파일을 제공하기 전에 특정 함수를 수행할 수 있는데 이때 수행되는 것이 Lambda@Edge 함수입니다. 유저에 따라 파일을 변경하거나 특정 유저의 요청을 무시하는 등 다이내믹하게 동작할 수 있습니다.

자 이제 감이 오시죠? 이제 이걸 이용해서 OG Tag를 변경해 주시면 됩니다.

 

유저의 요청은 아래와 같이 4단계로 나누어 이뤄집니다.

  • CloudFront가 최종 사용자로부터 요청을 수신한 후(최종 사용자 요청)
  • CloudFront가 오리진에 요청을 전달하기 전(오리진 요청)
  • CloudFront가 오리진으로부터 응답을 수신한 후(오리진 응답)
  • CloudFront가 최종 사용자에게 응답을 전달하기 전(최종 사용자 응답)

우리는 여기서 viewer request와 origin response에서 사용할 함수를 생성해야 합니다.

viewer request에서는 요청을 한 유저가 크롤러 봇인지 확인하여 request header에 크롤러 판별 값을 넣어줍니다.

origin response에서는 크롤러 판별 값을 확인한 후 크롤러인 경우 요청한 주소에 맞는 OG tag content를 생성하여 body에 넣어 사용자에게 넘겨줍니다. 이론상으론 별 것 아닌 것 같았지만 막상 진행을 해보니 난관이 많았습니다.

 

Lambda@Edge 함수 생성하기

2020-09-01 현재 Lambda@Edge 서비스는 us-east-1 리전에서만 제공됩니다.

위와 같이 리전을 변경 후 작업을 진행해 주세요.

 

그리고 Lambda 서비스로 들어가 좌측의 메뉴 중에 함수 서비스를 선택한 후 함수 생성을 선택합니다.

 

함수 생성 시 '새로 작성'을 선택하고 함수 이름은 원하는 대로 작성하면 됩니다. 저는 node.js를 이용하여 함수를 작성할 예정입니다. 이외에 python이나 ruby 등 다른 방법으로도 작성할 수도 있으니 편한 방법을 이용하세요.

 

lambda에서 실행한 로그를 기록하기 위해서 IAM 권한을 설정해줘야 하는데 함수 생성이 처음이라면 새 역할 생성을 선택하여 '기본 Lambda@Edge 권한 (CloudFront 트리거 용)'을 선택하고 역할 이름을 입력하면 새 역할이 생성됩니다.

다음부터 Lambda 함수를 생성할 때는 새 역할 생성할 필요 없이 기존 역할 중에 생성한 역할을 선택하면 됩니다.

 

Viewer Request 트리거 추가하기

const bot = /googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|kakaotalk-scrap|yeti|naverbot|kakaostory-og-reader|daum/g;

exports.handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const user_agent = request.headers['user-agent'][0]['value'].toLowerCase();

  if (user_agent) {
    const found = user_agent.match(bot.toLowerCase());
    request.headers['is-crawler'] = [
      {
        key: 'is-crawler',
        value: `${!!found}`,
      },
    ];
  }
  callback(null, request);
};

함수 생성 후 함수 코드 메뉴에 index.js 파일이 생성되어 있는데 이 부분에 위 코드를 삽입하고 저장합니다.

위 함수는 viewer request의 user-agent에 crawler-bot에 해당하는 문자열이 있는지 확인한 후 crawler-bot 유무를 request header에 'is-crawler' 프로퍼티로 추가한 후 요청을 다시 진행합니다.

 

테스트를 진행하고 싶다면 Save 버튼 옆의 Test 메뉴 우측을 클릭한 후 'Configure Events'를 클릭하여 테스트 이벤트를 생성합니다. 쉽게 테스트를 생성하려면 이벤트 템플릿에 'cloudfront-ab-test'를 선택한 후 user-agent의 value 및 다른 값들을 바꿔가며 테스트해보시면 됩니다. 테스트 전에 꼭 저장을 하고 테스트하셔야 작성한 내용이 반영됩니다.

 

테스트 완료 후 함수를 배포하려면 상단 디자이너 탭에서 '트리거 추가'를 수행합니다.

 

트리거 추가를 실행하면 위와 같은 메뉴가 뜨는데 CloudFront 트리거를 선택한 후 Lambda@Edge 배포를 클릭합니다.

 

위와 같이 새로운 창이 또 뜨는데 되는데 여기서 주의할 점은 '배포' 메뉴에서 cloudfront가 여러 개 존재할 경우 자동으로 첫 번째 cloudfront가 선택되어 있으니 꼭 우측 X를 클릭한 후 적용하려는 cloudfront를 수동으로 선택해 줍니다.

다음 CloudFront 이벤트에서는 '뷰어 요청'을 선택한 후 배포 확인도 체크한 후에 배포를 하면 새 버전의 함수가 선택한 cloudfront에 배포됩니다. 적용되는데 수분 정도 시간이 걸리고 진행 과정은 cloudfront 메뉴에서 확인 가능합니다.

 

Origin Response 트리거 추가하기

위의 방법과 유사하지만 추가 설정이 더 필요합니다. 우선 위의 방법대로 함수를 먼저 생성합니다.

그 후에 아래 이미지와 같은 파일들이 필요합니다.

외부 라이브러리를 사용하기 위해서는 위와 같이 프로젝트를 생성해서 node_modules 폴더를 가져와야 합니다.

이를 수행하기 위해서는 위의 파일들을 zip 파일로 묶어서 업로드해야 하는데 업로드하는 메뉴는 아래에 있습니다.

업로드할 프로젝트를 생성하기 위해서 터미널 프로그램을 켠 후 생성할 곳으로 이동합니다. 그 후

mkdir response-origin
cd response-origin
yarn init
yarn add axios url-pattern

수행하면 라이브러리가 추가됩니다.

 

'use strict';
const axios = require('axios');
const UrlPattern = require('url-pattern');

// origin-response
exports.handler = async (event, context, callback) => {
  const { request, response } = event.Records[0].cf;
  const { headers, uri } = request || {};

  const pattern = new UrlPattern('/*사용할 패턴*/');
  const match = pattern.match(uri);

  if (match) {
    let is_crawler = undefined;
    if ('is-crawler' in headers) {
      is_crawler = headers['is-crawler'][0].value.toLowerCase();
    }

    if (is_crawler === 'true') {
      const apiRes = await axios.get('/*사용할 API 주소*/');

      const { intro, title, thumb_nail_url } = apiRes.data.data;

      response.status = 200;
      response.headers['content-type'] = [
        {
          key: 'Content-Type',
          value: 'text/html',
        },
      ];
      // 꼭 캐시를 사용하지 않도록 설정해야 합니다.
      response.headers['cache-control'] = [
        {
          key: 'cache-control',
          value: 'no-cache, no-store, must-revalidate',
        },
      ];
      response.body = `<html><head>
                        <meta property="og:url" content="/*요청한 주소*/" />
                        <meta property="og:type" content="website" />
                        <meta property="og:locale" content="en_US" />
                        <meta property="og:title" content="${title}" />
                        <meta property="og:description" content="${intro}"/>
                        <meta property="og:image" content="${thumb_nail_url}" />
                        <meta property="og:image:type" content="image/png" />
                        <meta property="og:image:width" content="600" />
                        <meta property="og:image:height" content="600" />
                        <meta name="twitter:card" content="summary">
                        <meta property="fb:app_id" content="/*페이스북 app_id는 옵션*/" />
                        <title>${title}</title>
                      </head></html>`;
    }
  }

  callback(null, response);
};

index.js 파일을 생성한 후 위의 코드에서 /* */ 주석처리된 부분을 사용자에 맞게 수정한 후에 작성해서 넣으면 됩니다.

파일을 다 작성하였으면 프로젝트 전체를 압축한 후 함수 코드 에디터에 업로드하면 됩니다.

업로드 후 저장하고 테스트 코드도 작성한 후 테스트를 완료한 후 위와 같은 방법에서 Cloudfront 이벤트만 오리진 응답으로 설정해서 트리거를 추가하면 됩니다.

 

최종 테스트 수행하기

배포가 완료되었다면 최종 테스트를 수행해봐야 하는데 가장 쉬운 방법은

cards-dev.twitter.com/validator

developers.facebook.com/tools/debug

위 사이트에 방문하셔서 URL을 입력해보면 됩니다.

 

좀 더 자세한 정보가 필요하면 터미널 창에서 다음과 같이 입력했을 때 원하는 결과가 나오는지 확인하면 됩니다.

curl -v --compressed -H "Range: bytes=0-524288" -H "Connection: close" -A "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)" "테스트 URL"

 

캐시 설정

response.headers['cache-control'] = [
  {
    key: 'cache-control',
    value: 'no-cache, no-store, must-revalidate',
  },
];

origin response 응답을 보낼 땐 위의 예제 코드처럼 꼭 캐시 무효화를 넣어서 보내야 합니다. 그렇지 않으면 한 번 생성된 콘텐츠 캐시가 남아있어서 일반 사용자도 같은 응답을 받게 됩니다. 혹시 캐시가 남아서 같은 응답이 계속 오게 된다면 cloudfront 메뉴에서 create invalidation을 수행할 때 해당 폴더 하위 폴더(예를 들어 detail 하위의 캐시를 모두 제거하려면 /detail*)를 입력하여 캐시를 invaldate 해주면 됩니다.

 

글을 보고 이해가 가지 않거나 잘못된 부분 혹은 질문이 있으시면 언제든 댓글 남겨주세요. 감사합니다.

 

참조