![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1588638907769-image.png) # Lambda@Edge로 SPA 환경에서 SEO 최적화 하는 법 개인 블로그를 개발하면서 초기에 목표 했던 기초 작업들을 끝마쳤다. 그 중에서 SEO 설정하는 부분 때문에 고민을 많이 했는데 여기서 어떤 방식으로 해결했는지 자세히 알아보도록 하겠다. ## SPA (Single Page Application) 이란? SPA, 한글로 단일 화면 어플리케이션이란 무엇일까. 여기서 핵심적인 단어는 `Page` 라고 볼 수 있으며 Page는 화면 또는 `html` 파일을 지칭한다고 생각할 수 있다. 즉 단일 화면이란 `html` 파일이 하나만 존재한다는 뜻이며 화면 전환등의 효과가 모바일 어플리케이션 처럼 부드럽게 전환되도록 설계한 것이 SPA의 첫번째 목표이다. 전통적인 웹 어플리케이션은 메뉴에 링크를 눌러 보여주는 화면이 달라질 경우 `html` 파일 부터 `css`, `js` 파일등을 모두 다시 불러왔다. 그러나 SPA에서는 최초 한번만 `html` 을 불러온 후 `javascript`로 렌더링 하는 내용을 결정하는 방식이다. (물론 CSR 기준에서) SPA를 도입하므로써 얻는 장점은 많이 있다. 모바일 어플리케이션과 유사한 사용성을 제공하거나 화면을 컴포넌트 단위로 개발하여 재사용성이 높다거나 등.. 그러나 몇가지 환경에서는 SPA만으로는 해결하지 못하는 문제점들에 부딫히게 된다. ## SPA의 문제점 SPA 환경에서 거론되는 문제점들은 몇가지가 있는데 대표적으로는 아래와 같다. - 초기에 모든 리소스들을 받아오므로 로딩 시간이 길어진다. - 잘못 설계될 경우 오히려 성능 이슈가 발생할 수 있다. - 크롤링을 하는 봇이 어떤 문서인지 인식을 할 수가 없다. (구글 크롤링 봇 제외) ### 초기에 모든 리소스들을 받아오므로 로딩 시간이 길어진다. SPA에서는 최초 모든 리소스들을 불러오기 때문에 특정 페이지에 진입하지 않더라도 관련된 리소스들을 모두 가져온다. 이렇게 될 경우 최초 화면 진입시에 로딩시간이 조금 걸리게 된다. 몇 초 정도의 로딩시간은 무관할 경우 큰 문제는 없지만 사용자 경험이 최우선 되는 서비스라면 사용자가 기다려줄 인내심이 없을 수도 있다. 이러한 문제를 해결하기 위해선 lazy하게 해당 페이지 진입시 리소스들을 불러오게 하거나 렌더링 방식을 변경함으로써 해결할 수 있다. ### 잘못 설계될 경우 오히려 성능 이슈가 발생한다. 브라우저에서 `DOM`을 렌더링 할때 걸리는 시간이 오래 걸린다 판단하여 `DOM` 렌더링을 최적화 해주는 방식으로 SPA 환경에서는 성능을 향상 시켰다. `DOM` 에 새롭게 렌더링을 해야할 지 아니면 기존 내용과 동일하니 그대로 두어야 하는지를 결정하는 방법은 프레임워크마다 다른데 잘못 설계할 경우 같은 내용임에도 불구하고 새로운 데이터로 인식하여 매 순간마다 모든 컴포넌트들이 다시 렌더링 된다면 SPA 환경의 최적화가 의미가 없으며 오히려 더 나쁜 성능을 제공할 수 있다. ### 크롤링을 하는 봇이 어떤 문서인지 인식을 할 수가 없다. (구글 크롤링 봇 제외) SPA 환경에서 겪게되는 가장 큰 문제중 하나가 바로 SEO를 어떻게 처리하느냐 이다. 크롤링 봇은 `html` 문서 내의 `meta` 태그나 본문에 들어있는 내용을 읽어서 인식을 하는데 SPA에서는 최초 빈 `html` 파일에 `javascript`로 포함될 내용을 넣어주므로 최초에는 빈 `html` 파일 밖에 없다. 이때 크롤링 봇이 어떤 화면인지 분석할 수 없으며 이는 곧 SEO가 어렵게 되는 상황이 펼쳐진다. 물론 구글 크롤링 봇은 SPA 환경이더라도 어떤 내용을 포함하는지 인식할 수 있다고 하니 큰 문제가 되지는 않지만 문제는 SNS 환경이나 다른 크롤링 봇들 일 것이다. ## SPA환경에서 SEO 최적화 하는 방법 이러한 상황에 대비하여 SEO 최적화를 하는 방법은 여러가지가 있는데 대표적으로 `SSR(Server Side Rendering)`을 적용하는 방법이 있다. SSR이란 말 그대로 서버에서 렌더링을 우선 하여 전달하는 방식으로 완성된 `html` 파일을 클라이언트로 보내주기 때문에 크롤링 봇들이 인식을 할 수 있다. SSR을 지원하는 대표적인 프레임워크로는 `Nuxtjs` 나 `Nextjs` 등이 있다. 현재 블로그 서비스는 AWS 환경에서 Serverless 하게 제공 되고 있다. 즉 서버 없이 S3에 저장되어 있는 파일을 불러오기만 할 뿐 해당 내용을 가공해서 전달해주는 서버가 없기 때문에 SSR을 적용을 할 수가 없는 상황이었다. SEO는 포기해야 하나 고민하고 있을때 `CloudFront`가 좋은 대안을 제시해 주었다. ## AWS CloudFront와 Lambda@Edge AWS CloudFront는 CDN(Content Delivery Network) 서비스로 S3 URL을 연결해 줌으로써 단순히 파일을 제공하도록 설정할 수 있다. 이때 특정한 시점에 Lambda 함수를 실행 시켜주도록 하는데 이때 실행되는 것을 Lambda@Edge 라고 부른다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1588641200834-image.png) Lambda@Edge는 네가지 상황에서 실행 된다. - `viewer-request`, 클라이언트로부터 CloudFront에 요청이 도착했을 때 - `origin-request`, Origin Server에 요청하기 전 - `origin-response`, Origin Server로 부터 응답이 도착했을 때 - `viewer-request`, 클라이언트에게 응답을 돌려 주기 전 여기서 클라이언트란 당연히 실제 서비스를 사용하는 사람이 될 수도 있고 크롤링을 하는 봇일 될 수도 있다. 그렇다면 현재 요청이 봇인지 아닌지를 판별하여 응답을 하기 전에 크롤링에 필요한 데이터를 본문에 채워서 제공을 한다면 우리가 원하는 대로 정보를 인식하고 판별할 수 있을 것이다. 그렇게 하기 위해서는 두가지 상황에서 작업이 필요하다. - `viewer-request` : 요청이 온 클라이언트가 봇인지 아닌지 구분 - `origin-Response` : 봇이라면 응답을 해주기 전 데이터를 채워서 보내주어야 한다. 두 상황에서 Lambda@Edge를 사용하기로 하고 `serverless` 프레임워크를 사용해 배포를 적용하였다. Lambda@Edge를 사용하기 위해선 몇가지 제약 사항이 있는데 - **Memory와 Timeout 제한** : `viewer-request`와 `viewer-response`는 128MB Memory와 5초 timeout 제한이 있으며 `origin-request`와 `origin-response`는 3008MB Memory와 30초 시간 제한이 존재 - **Runtime 환경 제한** : Python 3.7, Node.js 10.x, Node.jx 8.10 환경만 지원한다. (2020년 5월 기준) 이 외에도 Lambda@Edge를 사용하기 위해서 유의해야 하는 점이 몇가지 있다. - 버지니아 북부(us-east-1) 리전에서만 배포 가능 - CloudWatch에 쌓이는 로그는 CloudFront에 요청한 지역에 쌓인다. (한국에서 접속했을 경우 ap-northeast-2 리전 CloudWatch에 로그가 쌓임) - CloudFront에 설정하기 위해서는 $latest가 아니라 버전을 명시해 주어야 한다. CRUD를 위한 Lambda개발도 `serverless` 프레임워크로 했기 때문에 이번에도 해당 프레임워크를 사용해서 작업을 진행 하려고 했다. 그러나 **[이미 배포된 CloudFront에 Lambda@Edge를 붙일 수는 없어서](https://forum.serverless.com/t/lambda-edge-with-an-existing-cloudfront-distirbution/9969/3 )** CloudFront로 연계하는 작업은 같이 처리하지 못하고 Console에 들어가서 직접 해줬다. Terraform으로 배포할 경우 연결하여 사용할 수 있는 것 같은데 다음번에는 Terraform으로 인프라 리소스를 관리하면서 연동하는 작업을 진행해 보려고 한다. 우선은 위에서 설명한 대로 두가지 상황에서 이벤트 처리를 진행했다. ### viewer-request ``` typescript const bot = /aolbuild|baidu|bingbot|bingpreview|msnbot|duckduckgo|adsbot-google|googlebot|mediapartners-google|teoma|slurp|yandex|bot|crawl|spider/g; export async function request(event: any, context: Context, callback: any) { 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); request.headers["is-crawler"] = [ { key: "is-crawler", value: `${!!found}` } ]; } callback(null, request); } ``` `event.Records[0].cf`에 `request`와 `response`가 들어있는데 여기서는 `request` 헤더에 있는 정보만 필요하므로 해당 정보만 불러와 `user-agent`가 봇인지 아닌지를 구분하고 `callback`으로 넘겨주는 간단한 작업을 수행하고 있다. ### origin-response ``` typescript export async function response(event: any, context: Context, callback: any) { try { const { request, response } = event.Records[0].cf; const pattern = new UrlPattern("/post(/*)"); const { headers, uri } = request || {}; const match = pattern.match(uri); if (match && match._) { // post로 조회하는 경우 let is_crawler: string; if ("is-crawler" in headers) { is_crawler = headers["is-crawler"][0].value.toLowerCase(); } if (is_crawler === "true") { // 크롤러 봇일 경우 response.status = 200; response.body = parsingHTML(html, title, desc, postId, content); response.headers["content-type"] = [ { key: "Content-Type", value: "text/html" } ]; } } callback(null, response); } catch (error) { console.log(error); } } ``` `origin-response`에서는 두가지를 체크한다. - post/* 로 들어온 요청인지 - 크롤러 봇인지 두가지 모두 해당될 경우 S3와 DynamoDB에서 관련된 정보를 가져와 HTML 파싱 후 해당 내용을 body에 담아서 `callback`으로 리턴해주기만 하면 된다. 이렇게 매우 간단하게 SEO 고민을 해결할 수 있었다. 배포 후 Lambda@Edge와 CloudFront를 연결 시켜 주고 나서 구글에 검색을 해보니 원하는 대로 검색이 되었다. ![](https://uzilog-upload.s3.ap-northeast-2.amazonaws.com/private/ap-northeast-2%3Ab6c10628-1f45-492c-a9eb-f54020bc8014/1588643777681-image.png) 물론 이것 외에도 상위로 검색하기 위해선 별도의 노력은 필요하지만 SPA의 단점은 SSR을 활용하지 않아도 보완할 수 있었다. ## 마무리 Lambda@Edge는 이 외에도 다양한 용도로 활용할 수 있다. S3를 각각 다른 곳에서 불러 오거나 Image를 리사이징해서 용량을 줄이거나 Cache 관련 설정을 변경하거나 등 사용자에게 콘텐츠를 제공 하기 전 작업이 필요할때 어떤 작업이든 수행할 수 있다. 이로써 Serverless한 환경을 구축하는데 장애물 없이 원하는 작업을 대부분 수행할 수 있는 것 같다. 한가지 아쉬운 점은 이 모든 구축 과정을 하나의 코드로 관리하지 못하고 따로 설정한 것이다. 다음번엔 Terraform으로 인프라 전체를 구축하여 관리하는 작업을 추가하면 CI/CD를 손쉽게 붙이고 추가 작업을 하더라도 같이 관리가 될 수 있을 것 같다. ### 참조 - [SPA SEO with Lambda@Edge](https://medium.com/@dilanthaprasanjith/solving-spa-meta-problems-with-the-power-of-lambda-edge-cdc8773e23cc) - [Dynamically Route Viewer Requests to Any Origin Using Lambda@Edge](https://aws.amazon.com/ko/blogs/networking-and-content-delivery/dynamically-route-viewer-requests-to-any-origin-using-lambdaedge/) - [Serverless - CloudFront](https://www.serverless.com/framework/docs/providers/aws/events/cloudfront/) - [Cache control with Lambda@Edge](https://geeks.uniplaces.com/cache-control-with-lambda-edge-95645b3aa4f0) - [Lambda@Edge Example Functions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html) - [Lambda@Edge를 통한 멀티리전 기반 클로벌 트래픽 길들이기](https://www.slideshare.net/awskorea/taming-multi-region-based-global-traffic-through-lambda-edge-94929766) - [10 AWS Lambda Use Cases to Start Your Serverless Journey](https://www.simform.com/serverless-examples-aws-lambda-use-cases/)