단일 페이지 애플리케이션의 SEO 가능성丨Angular 프로젝트를 위한 3가지 인덱싱 최적화 방안

本文作者:Don jiang

단일 페이지 애플리케이션(SPA)은 부드러운 사용자 경험 덕분에 현대 웹 개발의 주류 선택이 되었지만, SEO 효과는 동적 렌더링 문제로 인해 크게 저하되는 경우가 많습니다.

전통적인 검색 엔진 크롤러는 JavaScript를 해석하는 능력이 제한적이어서 핵심 콘텐츠를 인덱싱하지 못하는 경우가 많습니다.

Angular는 기업용 프런트엔드 프레임워크로 개발 효율성이 높지만, 기본적으로 생성되는 페이지 구조는 SEO 요구사항을 충족하기 어려운 경우가 많습니다.

Angular 프로젝트가 SPA의 장점을 유지하면서도 검색 엔진에 효율적으로 크롤링되도록 하려면 어떻게 해야 할까요?

단일 페이지 애플리케이션 SEO 가능성

서버 사이드 렌더링(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 // 라우트 자동 탐지 (사전에 라우트 목록을 내보내야 함)

}
}

빌드 실행:

npm run build && npm run prerender

생성된 정적 파일은 기본적으로 dist/<project-name>/browser 디렉터리에 출력됩니다.

방법 2: 서드파티 도구 (Prerender.io / Rendertron)

적용 대상: 라우팅이 복잡하거나 동적 파라미터(예: /product/:id)가 필요한 페이지.

작업 흐름:

Prerender 미들웨어 통합:

npm install prerender-node

Express 서버에 미들웨어 추가:

// server.ts
import * as prerender from 'prerender-node';
app.use(prerender.set('prerenderToken', 'YOUR_TOKEN'));

Prerender.io 대시보드에서 사전 렌더링할 라우팅 규칙 설정.

비교 및 선택 가이드:

  • 공식 방법: 고정된 라우팅과 적은 수의 경로를 가진 프로젝트에 적합하며, Angular 생태계에 의존하고 유지보수 비용이 낮음.
  • 서드파티 방법: 동적 파라미터 라우팅과 분산 렌더링이 필요한 대규모 프로젝트에 적합하지만, 유료이거나 자체 렌더링 서비스를 구축해야 함.

서버 호스팅 구성 팁

핵심 원칙: 서버/CDN이 사전 렌더링된 정적 HTML을 우선 반환하고, 클라이언트가 이후 인터랙션을 담당하도록 함.

호스팅 환경 및 설정 예시:

정적 서버 (예: Nginx):

server {
location / {
root /path/to/dist/browser;
try_files $uri $uri/index.html /index.html;
# 사전 렌더링된 파일(예: about.html)이 있으면 우선 반환, 없으면 index.html로 대체
}
}

CDN/S3 호스팅 (예: AWS S3 + CloudFront):

dist/browser 디렉터리를 S3 버킷에 업로드.

CloudFront 설정:

  1. 기본 루트 오브젝트를 index.html로 설정.
  2. 맞춤 오류 응답 설정: 404를 /index.html로 리다이렉트 (라우팅 미스매치 문제 해결).

Jamstack 플랫폼 (예: Netlify/Vercel):

netlify.toml에 리다이렉트 규칙 추가:

[[redirects]]

from = “/*”
to = “/index.html”
status = 200

자주 발생하는 문제 해결:

  • 라우팅 404 오류: 서버가 try_files 또는 index.html로 폴백하도록 설정되었는지 확인하세요.
  • 정적 파일이 업데이트되지 않음: CDN 캐시를 삭제하거나 파일 해시 버전 관리를 추가하세요.

자동화 업데이트 및 버전 관리

핵심 요구사항: 페이지 내용이나 데이터 소스가 변경될 때 자동으로 프리렌더링을 실행하고, 온라인 환경에 동기화합니다.

실현 방법:

정적 자원 버전 관리:

angular.json에서 빌드 파일에 해시를 추가해 캐시 문제를 방지하세요:

"outputHashing": "all" // 해시가 포함된 파일명 생성 (예: main.abc123.js)

CI/CD 프로세스 통합 (GitHub Actions 예시):

jobs:
deploy:
steps:
- name: 의존성 설치
run: npm install
- name: 빌드 및 프리렌더링
run: npm run build && npm run prerender
- name: S3에 배포
run: aws s3 sync dist/browser s3://your-bucket --delete

증분 프리렌더링 최적화:

변경된 페이지만 렌더링 (CMS 또는 API 훅과 연동 필요):

# 예시: API를 통해 업데이트된 페이지 목록 가져오기
UPDATED_PAGES=$(curl -s https://api.example.com/updated-pages)
npm run prerender --routes=$UPDATED_PAGES

모니터링 및 알림:

  • Lighthouse를 사용해 프리렌더링 페이지의 SEO 점수 검사.
  • Sentry를 설정해 클라이언트 라우팅 변경 후 JS 에러 모니터링.

동적 메타 태그 및 구조화 데이터 최적화

페이지 내용이 검색 엔진에 의해 크롤링되더라도, 표준 메타 태그(Meta Tags)와 구조화 데이터(Structured Data)가 없으면 검색 순위가 낮거나 검색 결과가 혼란스러울 수 있습니다.

예를 들어, 제목이 중복되거나 설명이 없고 제품 정보가 표시되지 않으면 크롤러가 페이지 가치를 이해하기 어렵고, 사용자가 검색 요약으로 관련성을 판단하기 힘듭니다.

동적 메타 태그 구현 방법






라우터 변경에 따른 메타 정보 동적 업데이트



핵심 목표

라우터 변화에 따라 제목, 설명, 키워드 등 메타 정보를 실시간으로 업데이트하여 모든 페이지가 동일한 메타 태그를 공유해 SEO 페널티를 받는 것을 방지합니다.

구체적인 작업

Angular의 Meta 서비스 사용

컴포넌트 내에서 Meta 서비스를 통해 태그를 동적으로 설정합니다. 예를 들어 상품 상세 페이지에서는:

// product.component.ts
ngOnInit() {
  this.meta.updateTag({ name: 'title', content: '상품 이름 - 브랜드명' });
  this.meta.updateTag({ name: 'description', content: '상품 요약 및 핵심 키워드 포함...' });
  this.meta.updateTag({ name: 'keywords', content: '키워드1, 키워드2, 키워드3' });
}

주의: 키워드 남용을 피하고, 설명은 자연스럽게 작성하며 사용자의 검색 의도를 포함해야 합니다.

라우터 감지 및 자동 업데이트

루트 컴포넌트나 라우터 가드에서 라우터 변화를 감지하고 이전 페이지의 메타 태그를 초기화합니다:

// app.component.ts
constructor(private router: Router, private meta: Meta) {
  this.router.events.pipe(
    filter(event => event instanceof NavigationEnd)
  ).subscribe(() => {
    this.meta.removeTag('name="description"'); // 이전 페이지의 description 제거
  });
}



});
}

소셜 공유 최적화

Open Graph(Facebook) 및 Twitter 카드 프로토콜에 맞춰, 전용 태그를 추가하세요:

this. meta. updateTag({ property: 'og:title', content: '상품 제목' });
this. meta. updateTag({ property: 'og:image', content: 'https://example.com/image.jpg' });
this. meta. updateTag({ name: 'twitter:card', content: 'summary_large_image' });

구조화 데이터의 유형과 적용 사례

핵심 가치
Schema 마크업(JSON-LD 형식)을 통해 페이지 콘텐츠 유형을 명확히 하여, 검색 결과에서 별점, 가격대 등 풍부한 미디어 표시 확률을 높입니다.

일반적인 적용 사례 및 구현 방법

상품 페이지 마크업

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "상품 이름",
"image": ["이미지 URL"],
"description": "상품 설명",
"brand": { "@type": "Brand", "name": "브랜드명" },
"offers": {
"@type": "Offer",
"price": "99.00",
"priceCurrency": "CNY"
}
}
script>

​글/블로그 마크업​​:

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "글 제목",
"datePublished": "2023-01-01",
"author": {
"@type": "Person",
"name": "작성자명"
}
}
script>

​FAQ 페이지 마크업​​:

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [{
"@type": "Question",
"name": "질문1",
"acceptedAnswer": {
"@type": "Answer",
"text": "답변 내용"
}
}, {
"@type": "Question",
"name": "질문2",
"acceptedAnswer": {
"@type": "Answer",
"text": "답변 내용"
}
}]
}
script>

검증 도구​​:

Canonical 태그와 다중 라우트 관리​

​문제 배경​​:SPA에서 다른 라우트 파라미터가 유사한 콘텐츠를 생성할 수 있습니다(예: 정렬 필터 /products?sort=price). 이로 인해 크롤러가 중복 페이지로 오인할 수 있습니다.

​해결책​​:

​Canonical 태그 설정​​:

페이지 내에서 주 버전 URL을 선언하여 권중 분산을 방지합니다:

// 컴포넌트 내에서 동적으로 설정
this. meta. updateTag({ rel: 'canonical', href: 'https://example.com/products' });

​필요 없는 파라미터 무시하기​​:

Angular 라우트 설정에서 UrlSerializer를 통해 URL 직렬화 규칙을 커스텀하여 관련 없는 파라미터를 필터링합니다:이 글은 HTML 코드가 포함된 블로그 글이며, 원문 구조(HTML 코드 포함)를 바꾸지 않고 한국어로 번역해 주세요. 최대한 자연스럽게 표현해 주세요.

// 사용자 정의 URL 파서
export class CleanUrlSerializer extends DefaultUrlSerializer {
parse(url: string): UrlTree {
// sort, page 같은 파라미터 제거
return super.parse(url.split('?')[0]);
}
}

AppModule에 등록:

providers: [
{ provide: UrlSerializer, useClass: CleanUrlSerializer }
]

robots.txt 크롤링 제어:

파라미터가 포함된 중복 페이지의 크롤링 방지:

User-agent: *
Disallow: /*?*

실제 프로젝트에서는 단계별 적용을 권장합니다: 초기에는 프리렌더링으로 핵심 페이지를 빠르게 커버하고, 중간 단계에서는 SSR을 도입해 동적 콘텐츠 크롤링 효율을 높이며, 지속적으로 구조화된 데이터를 개선해 나갑니다.

Picture of Don Jiang
Don Jiang

SEO本质是资源竞争,为搜索引擎用户提供实用性价值,关注我,带您上顶楼看透谷歌排名的底层算法。

最新解读
滚动至顶部