인프라

동적 렌더링을 통해 싱글페이지 어플리케이션(SPA) 의 SEO 를 관리하는법

consolelog 2025. 8. 2. 07:00

싱글페이지 어플리케이션(SPA) 은 뛰어난 사용자 경험과 빠른 전환 속도를 제공하지만, 검색엔진 최적화(SEO) 측면에서는 여러 제약이 따릅니다. 특히 Google, Bing 등의 검색 엔진 크롤러는 자바스크립트를 완벽하게 처리하지 못하거나, 처리하더라도 상당한 시간이 소요될 수 있어 초기 HTML이 빈 상태로 노출되는 문제가 발생합니다.

 

이를 해결하는 대표적인 방식 중 하나가 동적 렌더링 입니다.

 

이 글에서는 동적 렌더링의 개념과 구현 방식에 대해 알아보고, 예제를 통해 실제로 어떻게 동작하는지 살펴보겠습니다.

 

왜 SPA 는 SEO 에 불리할까?

SPA는 자바스크립트를 통해 클라이언트에서 콘텐츠를 렌더링합니다. 이는 사용자에게는 빠르고 부드러운 경험을 주지만, 검색 엔진 크롤러는 다음과 같은 문제를 겪습니다:

  • 최초 HTML이 텅 비어 있음 (<div id="app"></div>)
  • 메타 태그(title, description 등)가 클라이언트에서 동적으로 생성됨
  • OpenGraph, Twitter 카드 정보도 늦게 삽입되어 SNS 미리보기 실패

 

동적 렌더링이란?

동적 렌더링은 봇과 사람을 구분하여 다른 HTML을 제공하는 방식입니다.

 

사용자에게는 일반 SPA 처럼 자바스크립트 중심의 앱을 제공하고, 검색 엔진 크롤러에게는 서버 또는 프록시가 사전 렌더링된 HTML 을 반환하는 방식입니다.

 

Google 공식 문서에서도 SPA 에 대한 해결방법으로 동적 렌더링을 추천한 바 있습니다.

Dynamic Rendering | Google Search Central

 

 

동적 렌더링의 구현 방식

  1. SPA 초기화 시점에서 메타 태그 생성
    • SPA 는 서버에서 HTML 을 완성된 형태로 제공하지 않기 때문에, 각 페이지 진입 시점에 자바스크립트를 통해 메타 정보를 동적으로 생성해줘야 합니다.
  2. User-Agent 분석 및 요청 분기
    • 서버 또는 프록시 레이어에서 들어오는 요청의 User-Agent 를 분석해 검색엔진 크롤러인지 여부를 판별합니다.
    • 예: googlebot, bingbot, twitterbot, slackbot 등등
  3. 랜더링 서버 구성 (Headless 브라우저 기반)
    • 검색 봇의 요청일 경우, puppeteer 등 headless 브라우저를 사용해 SPA 를 실제로 렌더링하고, 렌더링이 완료된 HTML 을 반환합니다.
    • 추천하는 오픈소스: prerender

 

동적 렌더링의 장단점

 

장점

  • 기존 SPA 구조를 유지하면서 SEO 대응이 가능
    • SSR 이나 SSG 로 전체 아키텍처를 변경하지 않고도 검색엔진 최적화를 구현할 수 있습니다.
  • 점진적 도입 가능
    • 전체 페이지가 아닌 일부 주요 SEO 타깃 페이지만 우선 적용하는 방식으로 단계적 전환이 가능합니다.

 

단점

  • 추가 인프라 구성
    • 프록시 서버, 렌더링 서버(Puppeteer 등), 캐시 시스템 등 SPA 외부에 별도의 인프라가 필요합니다.
  • User-Agent 식별 관리의 어려움
    • 검색봇 종류는 많고 계속 추가되므로, 누락되면 SEO 반영이 되지 않을 수 있습니다.
  • 구글 정책 변경 리스크 존재
    • Google 은 동적 렌더링을 임시 솔루션으로 간주하고 있으며, 장기적으로는 SSR/SSG 를 권장하고 있습니다.
  • 캐시 관리를 잘못하면 클로킹(cloaking) 으로 오인될 수 있음
    • 검색 엔진에 제공되는 HTML과 실제 사용자에게 제공되는 내용이 불일치할 경우, Google 가이드라인을 위반한 것으로 간주되어 검색에서 불이익을 받을 수 있습니다. 따라서 봇과 사용자 모두에게 동일한 콘텐츠가 노출되도록 캐시 유효성 관리가 중요합니다.

 

 

동적 랜더링 구현 예제

이번 예제에서는 간단한 SPA 를 동적 렌더링 방식으로 구현해보고, 이를 통해 SEO 측면에서 어떤 차이가 발생하는지 직접 확인해보겠습니다.

 

우선 리엑트로 간단한 예제 파일을 만들어 보겠습니다.

 

home 과 about 페이지를 가진 react app

 

Home 과 About 페이지를 가진 간단한 SPA 입니다.

 

여기서 빌드시킨 파일을 Nginx 가 가리키도록 해보고 접속해보면 다음과 같은 페이지를 보게됩니다.

 

SPA 가 응답하는 html 파일

 

위의 그림에서 확인할 수 있듯이, SPA는 우리가 동적으로 설정한 meta 태그나 렌더링된 콘텐츠를 무시하고, 기본적인 HTML 구조만을 반환하는 모습을 보여줍니다.

 

이번에는 nginx 와 서버를 통해서 동적 렌더링을 구현해 차이점을 살펴보도록 하겠습니다.

 

server {
    listen 80;
    server_name spa.ddomoa.com;
    root /var/www/example;
    index index.html;

    # 정적 파일 캐싱 설정
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location / {
        try_files $uri @prerender;
    }

    location @prerender {
        set $prerender 0;
        
        # User-Agent 기반 봇 감지 (한국 검색엔진 추가)
        if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp|daumoa|naverbot") {
            set $prerender 1;
        }

        # _escaped_fragment_ 파라미터 체크
        if ($args ~ "_escaped_fragment_") {
            set $prerender 1;
        }

        # Prerender 자체 요청은 제외
        if ($http_user_agent ~ "Prerender") {
            set $prerender 0;
        }

        # 정적 파일들은 prerender 제외
        if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|woff2|svg|eot)") {
            set $prerender 0;
        }

        # 로컬 DNS resolver (일반 환경용)
        resolver 8.8.8.8;

        if ($prerender = 1) {
            # 셀프호스팅 prerender 서비스로 프록시
            set $prerender_service "localhost:3000";
            rewrite .* /$scheme://$host$request_uri? break;
            proxy_pass http://$prerender_service;
        }

        if ($prerender = 0) {
            rewrite .* /index.html break;
        }
    }

    # Prerender용 별도 location (프록시 헤더 설정 가능)
    location ~ ^/prerender/ {
        internal;
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }
}

 

위의 코드와 같이 nginx 를 세팅하여 User-Agent 를 검사하여, 검색엔진 크롤러의 경우 3000번 포트의 서비스로, 그 외의 일반 사용자는 SPA 로 만든 서비스로 이동하게 하였습니다.

 

그리고 https 로 서비스하기 위해 certbot 을 통해 ssl 인증서를 생성해 주었습니다.

 

const prerender = require("prerender");
const server = prerender({
  chromeLocation: "/usr/bin/google-chrome",
});
server.use(require("prerender-memory-cache"));
server.start();

 

 

서버는 위와 같은 코드로 설정하여 3000번 포트에서 실행하였습니다.

 

User-Agent 를 googlebot 으로 변경하여 접속한 경우 응답받은 html

 

 

 

위의 그림과 같이 User-Agent 를 googlebot 으로 변경후 접속한 경우, 응답받는 html 에 우리가 동적으로 설정한 meta 태그나 렌더링된 콘텐츠가 존재하는것을 확인할 수 있습니다.

 

마치며

여러 단점에도 불구하고, 기존에 SPA로 구축된 서비스를 크게 수정하지 않고도 SEO를 적용할 수 있다는 점은 동적 렌더링 방식의 큰 장점입니다. 특히 빠르게 SEO 대응이 필요한 상황에서는 현실적인 대안이 될 수 있습니다.

 

다만, 동적 렌더링은 근본적인 해결책이 아닌 임시방편에 가깝습니다. 장기적으로는 SEO와 성능, 유지보수 측면에서 SSR(Server-Side Rendering) 또는 SSG(Static Site Generation) 방식으로의 전환을 검토하는 것을 권장드립니다.

 

그럼 여기까지 마치며, 다음에도 도움이 될 수 있는 글로 돌아오겠습니다.

 

읽어주셔서 감사합니다.