들어가기에 앞서!
보다 쉽게 내용을 이해하실 수 있도록, 아래 콘텐츠를 먼저 읽어보시길 추천드립니다.
들어가며
안녕하세요, 넥슨크리에이터즈팀 김영진입니다.
저희 팀은 캠페인 서비스를 통해 넥슨 게임 크리에이터에게 다양한 콘텐츠 아이디어를 제공하고, 전 세계 유저에게 크리에이터 정보를 전달함으로써 크리에이터와 서포터가 함께 성장할 수 있는 Creators Platform을 운영하고 있습니다.
넥슨 크리에이터즈 공식 홈페이지
저희 Creators Platform 웹 페이지는 전 세계 다양한 지역에 서비스를 제공하고 있으며, 사용자에게 빠르고 안정적인 리소스 전달을 위해 CDN(Content Delivery Network) 기반으로 운영되고 있습니다. 해당 서비스의 특성상 페이지 수가 시간에 따라 동적으로 증가하고, 사용자에게는 실시간 데이터 제공이 필요합니다. 이러한 요구사항을 충족하기 위해 웹 페이지는 Client Side Rendering(CSR) 방식으로 개발하고 있습니다. CSR 방식은 빠른 인터랙션과 유연한 화면 구성 측면에서 장점이 많은 렌더링 방식입니다.
하지만 CSR 방식에는 의외의 허점이 있었습니다. 바로 검색 엔진 최적화(SEO)였습니다.
1. CSR 기반 웹 페이지에서 발생하는 SEO 이슈 정리
1-1. CSR 기반 색인 누락 이슈
검색 엔진에 웹페이지가 노출되기 위해서는, 해당 페이지의 콘텐츠가 색인(Indexing) 과정에서 수집되어야 합니다. 검색 엔진은 HTML 문서를 파싱하고, 이 안의 콘텐츠를 바탕으로 검색 결과에 어떤 정보를 노출할지를 결정합니다.
하지만 CSR(Client Side Rendering) 방식에서는 초기 HTML 문서에 실질적인 콘텐츠가 포함되지 않고, JavaScript가 실행된 이후에야 콘텐츠가 화면에 렌더링됩니다. 이로 인해 검색 엔진이 페이지를 수집하는 시점에 따라 콘텐츠가 없는 빈 HTML 문서가 색인되는 현상이 발생할 수 있습니다.
그림 1: JavaScript 실행 전 HTML 문서에는 실질적인 콘텐츠가 포함되지 않음
이 구조적 한계로 인해 실제로 다음과 같은 문제가 나타납니다:
•
콘텐츠 자체가 색인되지 않아 검색 결과에 노출되지 않음
•
로딩 메시지 등 임시 상태 정보가 검색 결과에 노출
•
페이지 제목이나 설명이 기본값으로 남아 정확한 정보 전달 실패
그림 2: 콘텐츠가 아예 누락된 채 검색 결과에 노출되지 않는 예시
그림 3: 변경된 제목이 반영되지 않고 기본값으로 노출된 예시
그림 4: 실제 콘텐츠는 보이지 않고 기본 문구만 색인된 예시
오늘날 Google과 Naver는 JavaScript를 어느 정도 해석할 수는 있지만, 완전한 렌더링이나 정확한 색인은 여전히 어렵습니다. 특히 JavaScript 실행 전인 Suspense 상태 에서 색인이 진행되는 경우, 로딩 메시지나 임시 콘텐츠가 색인에 사용될 수 있습니다.
그림 5: Suspense 상태에서 임시 메시지가 색인된 사례
이로 인해 나타나는 대표적인 이슈는 다음과 같습니다:
•
콘텐츠가 아직 로드되지 않은 상태의 로딩 메시지가 색인됨
•
그 결과, 의도한 정보가 아닌 임시 상태의 콘텐츠가 검색 결과에 노출
이러한 현상이 반복되면, 유사한 제목과 메타 정보를 가진 페이지들이 중복으로 색인되는 문제가 발생하고, 심한 경우 검색 엔진이 해당 페이지를 아예 제외시켜버리는 색인 실패 현상으로 이어지기도 합니다.
그림 6: Google Search Console에서 중복 페이지 색인 오류 발생 사례
1-2. 동적 콘텐츠의 검색 엔진 미색인 이슈
이제는 구조적인 문제뿐 아니라, 콘텐츠 생성 방식 자체가 SEO에 미치는 영향도 함께 고려해야 합니다.
저희 넥슨 크리에이터 서비스는 크리에이터를 모집하고, 상세 정보를 제공하며, 다양한 캠페인을 운영하기 위해 전용 페이지를 운영 중입니다. 이들 페이지는 대부분 크리에이터 승인, 캠페인 등록 등의 이벤트에 따라 실시간으로 생성 되는 구조를 가지고 있습니다.
그림 7: 크리에이터즈 둘러보기 페이지
그림 8: 캠페인 목록 페이지
하지만 이러한 크리에이터 및 캠페인 상세 페이지는 동적으로 생성되는 구조이기 때문에, 검색 엔진이 해당 콘텐츠를 실시간으로 인지하지 못하는 문제가 지속적으로 발생하고 있습니다. 그 결과, 많은 페이지가 검색 엔진에 정상적으로 색인되지 않거나 아예 노출되지 않는 현상이 자주 나타나고 있습니다.
이러한 색인 누락 현상은 크리에이터 정보를 통해 유저 유입을 유도해야 하는 플랫폼에 있어 치명적인 문제가 됩니다. 검색 엔진에 제대로 노출되지 않으면, 콘텐츠의 접근성은 급격히 낮아지고 신규 유저 유입, 캠페인 참여, 크리에이터 홍보 등 모든 지표에 부정적인 영향을 미치게 됩니다.
결과적으로, 서비스의 핵심 기능이 제 역할을 하지 못하게 되고, 플랫폼의 성장 가능성 자체가 크게 제한되는 상황으로 이어집니다.
2. 문제 해결 과정
이전의 1편에서 소개한 것처럼, 저희는 AWS Lambda를 활용해 CSR 환경에서도 Open Graph 메타 정보를 제공하고 있습니다. 이 접근 방식은 SNS 공유 시 풍부한 미리보기 정보를 제공해 주는 데 매우 효과적이었고, 동일한 아이디어를 검색 엔진 최적화(SEO)에도 적용할 수 있을 것이라고 판단했습니다.
CSR 방식의 특성상, 콘텐츠는 사용자의 브라우저에서 JavaScript가 실행된 이후에야 화면에 렌더링 됩니다. 이 때문에 검색 엔진이 페이지를 크롤링하는 시점이 콘텐츠 로딩보다 빠를 경우, 빈 HTML이 색인되는 문제가 발생할 수 있습니다. 물론, 최근 검색 엔진이 JavaScript를 어느 정도 실행할 수 있을 만큼 발전하긴 했지만, 색인이 정확히 언제 수행되는지는 여전히 예측할 수 없습니다. 이로 인해 콘텐츠가 완전히 로딩되기 전, 즉 Suspense 상태에서 색인이 진행되는 리스크는 여전히 존재합니다.
처음에는 이 문제를 해결하기 위해 Lambda를 활용한 SSR(Server Side Rendering) 기반 hydrate 전략을 고민했습니다.
1.
Lambda에서 API 데이터를 받아 window 객체에 삽입하고,
2.
클라이언트 측에서 해당 데이터를 활용해 hydrate 시 캐싱하여 콘텐츠를 빠르게 렌더링
이 방식은 Suspense 상태를 피할 수 있는 방법이긴 했지만, 다음과 같은 구조적 단점이 존재했습니다:
•
렌더링 방식 변경으로 인한 서비스 동작 방식 변경
•
Lambda 응답을 캐싱할 경우, 실시간 데이터와의 불일치 가능성
•
실시간 데이터 동기화를 위한 코드 복잡도 증가 및 관리 이슈 발생
결과적으로, 해당 방식은 기술적 리스크 대비 효과가 제한적이라 판단해 우선순위를 낮추기로 했습니다.
2-1. 요청자 기반 렌더링 전략
보다 본질적인 해결책은, 검색 엔진 크롤러가 CSR 기반 JavaScript를 해석하는 타이밍을 제어하는 것이었습니다. 저희는 이 관점을 바탕으로, 검색 엔진과 일반 사용자에 대해 서로 다른 렌더링 전략을 적용하는 방향으로 전환했습니다.
저희는 CloudFront Function을 활용해, 요청자가 검색 엔진 크롤러인지 일반 사용자인지를 다음과 같이 구분했습니다:
1.
Viewer Request 단계에서 User-Agent 값을 분석하여 요청자 유형을 판별
2.
검색 엔진 크롤러로 판단되면 HTTP Header에 x-is-bot이라는 커스텀 필드를 삽입
그림 9: CloudFront Function을 통한 요청 분기와 Lambda 응답 처리 구조
이렇게 요청자를 식별한 후, Lambda는 x-is-bot 헤더를 기준으로 서로 다른 방식의 HTML 응답을 제공합니다:
•
검색 엔진 크롤러 요청 시: Lambda가 서버 측에서 SSR로 정적 HTML을 생성
•
일반 사용자 요청 시: 기존의 CSR 방식 유지
이 전략을 도입함으로써 다음과 같은 효과를 얻을 수 있었습니다:
•
Suspense 상태 색인 방지: 로딩 중 상태가 검색 결과에 노출되는 문제 해결
•
중복 콘텐츠 오류 예방: 정적 HTML 제공으로 검색 엔진이 콘텐츠를 정확히 구분
•
콘텐츠 노출 증가: SEO 품질 개선으로 검색 결과 노출률 증가
그림 10: 요청자에 따라 HTML 렌더링을 분기하는 구조
단순히 렌더링 방식을 분리한 것에 그치지 않고, CloudFront의 캐시 정책(Cache Policy) 또한 요청자 유형에 따라 분기 처리했습니다:
•
Viewer Request 단계에서 추가된 x-is-bot 헤더를 기준으로
•
검색 엔진과 일반 사용자 요청을 서로 다른 캐시 경로로 분리
이를 통해 다음과 같은 실질적인 효과를 얻을 수 있었습니다:
•
불필요한 Lambda 호출 감소: 캐시 된 응답 활용으로 리소스 절약
•
Cache Hit률 향상: 요청 유형에 최적화된 캐시 전략 적용
•
비용 절감 + 성능 향상: 운영 효율성과 사용자 응답 속도 모두 개선
2-2. 실시간 Sitemap 생성 전략
크리에이터 전용 페이지와 캠페인 상세 페이지는 다음과 같은 이벤트를 통해 실시간으로 생성됩니다:
•
크리에이터 승인 완료 시 전용 페이지 생성
•
게임 사업팀의 캠페인 등록 시 캠페인 페이지 생성
이처럼 콘텐츠가 계속해서 동적으로 추가되는 구조에서는, 정적인 Sitemap을 수동으로 관리하는 방식으로는 한계가 명확합니다.
그래서 저희는 크롤링 봇이 Sitemap을 요청할 때마다, Lambda가 실시간으로 Sitemap을 생성해 응답하는 구조를 설계했습니다.
1.
Lambda가 API를 통해 현재 존재하는 페이지 목록을 실시간으로 조회
2.
조회한 데이터를 기반으로 동적으로 Sitemap XML을 생성하여 응답
그림 11: 실시간 Sitemap 응답 처리 흐름
저희는 Sitemap을 아래와 같은 Index 기반 구조로 구성했습니다.
sitemap.xml (Index 파일)
•
sitemap-creator.xml
•
sitemap-campaign.xml
•
sitemap-center.xml
•
sitemap-static.xml
각 세부 Sitemap 파일에는 해당 영역의 페이지 목록이 담겨 있으며, 다국어 페이지 지원을 위해 hreflang 태그도 함께 포함 되어 있습니다.
그림 12: Sitemap Index 구조 시각화
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://creators.nexon.com/sitemap-static.xml</loc>
<lastmod>yyyy-mm-dd</lastmod>
</sitemap>
<sitemap>
<loc>https://creators.nexon.com/sitemap-creator.xml</loc>
<lastmod>yyyy-mm-dd</lastmod>
</sitemap>
<sitemap>
<loc>https://creators.nexon.com/sitemap-center.xml</loc>
<lastmod>yyyy-mm-dd</lastmod>
</sitemap>
<sitemap>
<loc>https://creators.nexon.com/sitemap-campaign.xml</loc>
<lastmod>yyyy-mm-dd</lastmod>
</sitemap>
</sitemapindex>
JavaScript
복사
Creators Platform의 Sitemap 예시
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
>
...
<url>
<loc>https://creators.nexon.com/kr/campaign/104</loc>
<xhtml:link
rel="alternate"
hreflang="ko"
href="https://creators.nexon.com/kr/campaign/104"
/>
<lastmod>yyyy-mm-dd</lastmod>
</url>
<url>
<loc>https://creators.nexon.com/kr/campaign/98</loc>
<xhtml:link
rel="alternate"
hreflang="ko"
href="https://creators.nexon.com/kr/campaign/98"
/>
<lastmod>yyyy-mm-dd</lastmod>
</url>
...
</urlset>;
TypeScript
복사
Lambda기반 동적 Sitemap 예시
2-3. 개선 성과
이와 같은 렌더링 전략과 동적 Sitemap 구성을 통해, Google 검색 엔진에 색인된 페이지 수는 약 300건에서 1,000건 이상으로 대폭 증가하는 의미 있는 성과를 얻을 수 있었습니다.
그 중심에는 바로 AWS Lambda 기반의 유연한 처리 구조가 있었습니다.
3. SEO를 뒷받침하기 위한 Lambda 디버깅
앞선 콘텐츠에서는 다음과 같은 주제를 다루었습니다:
•
번들링을 통한 코드 관리 방식
•
테스트 코드 기반의 안정성 확보 전략
이번 콘텐츠에서는 그 연장선에서, Lambda 이슈를 얼마나 빠르게 식별하고 대응할 수 있었는지, 저희 팀이 실제로 적용한 사례와 구체적인 방법을 중심으로 공유드리고자 합니다.
3-1. Lambda의 역할이 확대되면서 생긴 고민
앞서 말씀드린 바와 같이, 현재 넥슨 Creators Platform에서는 Lambda가 다양한 핵심 기능을 담당하고 있습니다.
•
Open Graph 메타 태그 삽입을 통한 SNS 최적화
•
요청자 기반 SSR/CSR 분기 처리로 SEO 대응
•
실시간 Sitemap(XML) 생성으로 검색 엔진 색인 최적화
이처럼 점차 복잡하고 다양한 역할을 수행하게 되면서, 저희는 무엇보다도 신속하고 유연한 디버깅 환경을 갖추는 것이 중요하다고 판단했습니다.
3-2. 기존 방식의 한계
처음에는 코드 변경이 있을 때마다 Lambda에 배포한 후 테스트를 진행해야 했습니다. 하지만 이 방식은 반복되는 작업과 긴 대기 시간으로 인해 다음과 같은 문제를 유발했습니다:
•
개발 속도가 전반적으로 지체
•
반복적인 배포와 테스트로 인한 리소스 낭비
•
에러 확인과 수정까지 오랜 시간이 소요
특히 페이지 렌더링 구조처럼 민감한 기능을 자주 수정하는 상황에서는, 이러한 디버깅 프로세스가 너무 많은 비용을 요구했습니다.
3-3. Express 기반 로컬 디버깅 환경 구축
이 문제를 해결하기 위해, 저희는 Lambda 핸들러를 Express 기반의 로컬 서버에서 직접 실행할 수 있는 구조를 만들었습니다. 핵심은 CloudFront의 Behavior 로직을 그대로 로컬에 Mocking하여, 실제 배포 환경과 거의 유사하게 Lambda 코드를 테스트할 수 있도록 구성하는 것이었습니다.
// app.cloudFront.ts
const CALL_LAMBDA = async (req: Request, res: Response) => {
try {
const result: APIGatewayProxyResultV2<{ statusCode: number; headers: Object; body: string }> =
await handler(requestToLambdaConverter(req,res));
...
res.status(result.statusCode).send(result.body);
} catch (error) {
res.status(500).send({ message: 'Lambda execution error' });
}
};
const CF_BEHAVIOR_PATH_PATTERN__MIDDLE_WARE = {
'/sitemap*.xml': [/^\/sitemap(?:-[a-zA-Z0-9-]+)?.xml$/, CALL_LAMBDA],
'/*.??': ['/*.[a-zA-Z0-9]{2}$/', CALL_S3('/*.??')],
'/*.???': ['/*.[a-zA-Z0-9]{3}$/', CALL_S3('/*.???')],
'/*.????': ['/*.[a-zA-Z0-9]{4}$/', CALL_S3('/*.????')],
'*': ['*', CALL_LAMBDA],
} as const;
TypeScript
복사
cloudFront를 모방한 Express 로직 1
// app.ts
...
app.get(...CF_BEHAVIOR_PATH_PATTERN__MIDDLE_WARE['/sitemap*.xml']);
app.get(...CF_BEHAVIOR_PATH_PATTERN__MIDDLE_WARE['/*.??']);
app.get(...CF_BEHAVIOR_PATH_PATTERN__MIDDLE_WARE['/*.???']);
app.get(...CF_BEHAVIOR_PATH_PATTERN__MIDDLE_WARE['/*.????']);
app.get(...CF_BEHAVIOR_PATH_PATTERN__MIDDLE_WARE['*']);
...
TypeScript
복사
cloudFront를 모방한 Express 로직 2
3-4. 결과적으로 얻은 효과
이 구조 덕분에 기존의 Lambda 핸들러 코드를 전혀 수정하지 않고도, 로컬에서 Lambda 함수와 동일한 방식으로 실행하고 디버깅할 수 있는 환경이 마련되었습니다.
•
배포 없이도 코드 테스트 가능: 변경사항을 즉시 확인
•
디버깅 속도 향상: 빠르게 이슈를 찾고 수정
•
개발 효율과 품질 모두 개선: 운영 리스크 최소화
나가며
그림 13: 넥슨 크리에이터즈 크리에이터 상세 정보 SEO(좌)와 캠페인 상세 정보 SEO
CSR 기반 웹 서비스는 여전히 SEO 측면에서 뚜렷한 한계를 지니고 있습니다. 그리고 크리에이터 정보와 캠페인 콘텐츠의 검색 노출이 핵심인 Creators Platform에서는, 이 한계가 곧 서비스의 성장 자체를 막는 치명적인 장애물로 작용했습니다.
이를 해결하기 위해 저희 팀은 다음과 같은 전략을 도입했습니다:
•
사용자와 크롤러를 구분한 요청자 기반 렌더링 분기
•
콘텐츠 실시간 반영을 위한 동적 Sitemap 제공
이 두 가지 접근을 통해 검색 엔진 색인 품질을 획기적으로 개선할 수 있었고, 실질적인 노출 증가와 사용자 유입 확대라는 성과로 이어졌습니다. 또한, Lambda 코드를 로컬에서 직접 디버깅할 수 있는 개발 환경을 구축하면서, 개발 생산성을 높이고 배포 전 품질 안정성까지 확보하는 기반을 마련할 수 있었습니다.
무엇보다 강조하고 싶은 점은, 검색 노출은 단순한 마케팅 도구가 아니라, 콘텐츠 플랫폼의 사용자 접근성과 성장 가능성을 결정짓는 핵심 기능이라는 점입니다.
Lambda 기반의 SEO 대응 전략은 CSR 구조를 유지하면서도 검색 최적화를 가능하게 해주는, 지속 가능한 기술 성장의 필수 해법입니다. 넥슨 크리에이터즈팀은 더 나은 사용자 경험과 더 많은 유저 접점을 만들기 위해, 오늘도 기술을 끊임없이 고민하고 실험합니다.
감사합니다