단일 페이지 애플리케이션(SPA)은 부드러운 사용자 경험 덕분에 현대 웹 개발의 주류 선택이 되었지만, SEO 효과는 동적 렌더링 문제로 인해 크게 저하되는 경우가 많습니다.
전통적인 검색 엔진 크롤러는 JavaScript를 해석하는 능력이 제한적이어서 핵심 콘텐츠를 인덱싱하지 못하는 경우가 많습니다.
Angular는 기업용 프런트엔드 프레임워크로 개발 효율성이 높지만, 기본적으로 생성되는 페이지 구조는 SEO 요구사항을 충족하기 어려운 경우가 많습니다.
Angular 프로젝트가 SPA의 장점을 유지하면서도 검색 엔진에 효율적으로 크롤링되도록 하려면 어떻게 해야 할까요?
Table of Contens
Toggle서버 사이드 렌더링(SSR)으로 동적 콘텐츠 크롤링 문제 해결
SPA의 SEO 문제는 대부분 동적 렌더링 메커니즘에서 비롯됩니다. 페이지 콘텐츠가 클라이언트에서 JavaScript로 생성되기 때문입니다.
하지만 전통적인 검색 엔진 크롤러(예: 초기 Google 크롤러)는 JS 실행이 완전하지 않거나 지연될 수 있어, 핵심 콘텐츠를 제대로 수집하지 못할 수 있습니다.
Angular가 생성하는 페이지가 클라이언트 렌더링에만 의존하면, 크롤러에 반환되는 HTML은 빈 껍데기일 수 있어, 인덱싱에 큰 영향을 미칩니다.
Angular Universal 설정 및 배포
핵심 목표 : 서버에서 정적 HTML을 생성해 크롤러와 사용자에게 바로 반환하여 클라이언트 JS 렌더링 의존을 피하는 것입니다.
구체적인 단계 :
설치 및 초기화 : Angular CLI를 통해 Angular Universal을 빠르게 통합합니다:
ng add @nguniversal/express-engine # SSR에 필요한 의존성과 서버 파일 자동 설정
생성된 서버 진입 파일(예: server.ts
)이 라우트 요청을 처리하고 페이지를 렌더링합니다.
서버 사이드 데이터 선취 :
컴포넌트에서 TransferState
서비스를 사용하여 API 데이터를 서버에서 클라이언트로 전달해 중복 요청을 방지합니다:
// 서버 사이드 렌더링 시 데이터 가져오기
if (isPlatformServer(this.platformId)) {
this.http.get('api/data').subscribe(data => {
this.transferState.set(DATA_KEY, data); // TransferState에 저장
});
}
// 클라이언트는 TransferState에서 직접 데이터 읽기
if (isPlatformBrowser(this.platformId)) {
const data = this.transferState.get(DATA_KEY, null);
}
운영 환경 배포 :
PM2나 Docker를 사용해 Node.js 서버를 배포하고, 프로세스 관리 및 부하 분산을 구성합니다.
Gzip 압축과 캐싱(Nginx 리버스 프록시 등)을 활성화해 서버 부하를 줄입니다.
렌더링 오류(예: API 타임아웃)를 로그에서 모니터링해 빈 페이지 반환을 방지합니다.
첫 화면 콘텐츠 최적화 전략
핵심 원칙 : 크롤러가 “첫눈에” 핵심 정보(예: 제목, 제품 설명)를 완벽히 볼 수 있도록 하는 것입니다.
최적화 방법:
핵심 콘텐츠 우선 렌더링:
서버 사이드 렌더링 단계에서 첫 화면에 필요한 데이터를 동기적으로 강제로 로드합니다. 예를 들어:
// 라우트가 해석되기 전에 데이터 미리 로드
resolve():
Observable<Product> {
return
this.http.get('api/product');
}
Angular의 Resolve
가드를 활용하여 페이지 렌더링 전에 데이터가 준비되었는지 보장합니다.
HTML 크기 축소:
첫 화면에 필요하지 않은 서드파티 스크립트(예: 광고, 통계 코드)를 제거하고 클라이언트에서 지연 로드합니다.
중요한 CSS 스타일은 critical
도구로 추출하여 인라인 처리해 렌더링 차단을 줄입니다.
클라이언트 깜박임 방지:
app.component.html
에서 렌더링이 완료되지 않은 UI를 숨겨 크롤러가 중간 상태를 수집하는 것을 막습니다:
<div *ngIf="isBrowser || isServer" class="content">
div>
라우팅 및 동적 매개변수 호환 처리
일반 문제: 동적 URL(예: /product/:id
)은 크롤러가 모든 페이지를 탐색하지 못하게 할 수 있습니다.
해결책:
서버 라우트 구성:
Express 서버에서 모든 Angular 라우트를 매칭하여 어떤 경로라도 해당 페이지의 사전 렌더링된 HTML을 반환하도록 합니다:
// server.ts에서 와일드카드 라우트 설정
server.get('*', (req, res) => {
res.render(indexHtml, {
req,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
});
});
동적 매개변수 처리:
PlatformLocation
을 사용해 현재 URL 매개변수를 얻고 서버에서 해당 내용을 렌더링합니다.
export class ProductComponent implements OnInit {
productId: string;
constructor(private platformLocation: PlatformLocation) {
const path = this.platformLocation.pathname; // 경로를 가져옵니다 예: "/product/123"
this.productId = path.split('/').pop();
}
}
정적 사이트맵 생성:
빌드 단계에서 모든 동적 라우트를 순회하며, 전체 URL을 포함한 sitemap.xml
을 생성하고 검색 엔진에 적극적으로 제출합니다.
정적 페이지 사전 렌더링
핵심 로직은: 빌드 단계에서 각 라우트별로 미리 정적 HTML 파일을 생성해 서버나 CDN에 바로 호스팅하는 것입니다. 크롤러가 페이지를 요청하면 동적 렌더링 없이 미리 생성된 완전한 콘텐츠를 바로 반환합니다.
예를 들어, 100개의 페이지가 있는 공식 홈페이지의 경우, 빌드 시점에 모든 페이지 HTML을 생성하면 크롤러가 모든 내용을 탐색할 수 있고, 실시간 서버 계산이 필요 없습니다.
정적 HTML 생성 방법 두 가지
핵심 로직: 빌드 단계에서 모든 라우트를 순회하며 해당 페이지의 정적 HTML 파일을 미리 생성해 서버나 CDN에 바로 호스팅하며, 동적 렌더링이 필요 없습니다.
방법 1: Angular 공식 도구 (@angular/cli
+ prerender
)
설정 단계:
의존성 설치:
ng add @nguniversal/express-engine # SSR 기본 설정 활성화
angular.json
수정하여 프리렌더 빌드 명령 추가:
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routes": ["/", "/about", "/contact"], // 프리렌더할 라우트를 수동으로 지정
"guessRoutes": true // 라우트 자동 탐지 (사전에 라우트 목록을 내보내야 함)
}
}