angular-ssr은(는) 무엇을 하나요?
Angular v20+에서 @angular/ssr을 사용하여 서버사이드 렌더링(SSR) 및 수화(hydration)를 구현합니다. SSR 설정, 수화 전략, 정적 페이지 사전 렌더링, 브라우저 전용 API 처리에 사용됩니다. SSR 구성, 수화 불일치 수정, 라우트 사전 렌더링, 또는 코드의 SSR 호환성 작업 시 트리거됩니다.
설정
Angular CLI 프로젝트에 SSR을 추가합니다:
ng add @angular/ssr
기존 프로젝트에 수동으로 추가할 수도 있습니다:
npm install @angular/ssr
주요 작업
SSR 구성 확인
angular.json 파일에 SSR 관련 설정이 포함되어 있는지 확인하세요:
{
"projects": {
"my-app": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"ssr": {
"entry": "src/server.ts"
},
"prerender": {
"routes": [
"/",
"/about",
"/contact"
]
}
}
}
}
}
}
}
서버 부트스트랩 (server.ts)
기본 server.ts 파일:
import 'zone.js/dist/zone-node';
import { ngExpressEngine } from '@angular/ssr';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/my-app/browser');
const indexHtml = join(distFolder, 'index.html');
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
run();
수화 설정
Angular v20+에서는 provideClientHydration을 사용하여 다양한 수화 전략을 구성할 수 있습니다:
import { provideClientHydration, withNoHttpTransferCache, withEventReplay } from '@angular/platform-browser';
// 애플리케이션 구성에 추가
bootstrapApplication(AppComponent, {
providers: [
provideClientHydration(
withNoHttpTransferCache(),
withEventReplay()
)
]
});
부분 수화 (Angular v20+)
부분 수화를 활성화하려면:
import { provideClientHydration, withPartialHydration } from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [
provideClientHydration(
withPartialHydration()
)
]
});
브라우저 전용 API 처리
서버에서 브라우저 전용 API를 사용할 때 오류를 방지합니다:
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { PLATFORM_ID, inject } from '@angular/core';
export class MyComponent {
private platformId = inject(PLATFORM_ID);
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// 브라우저에서만 실행되는 코드
const token = localStorage.getItem('auth-token');
// ...
}
if (isPlatformServer(this.platformId)) {
// 서버에서만 실행되는 코드
// (예: 메타데이터 설정)
}
}
}
혹은 afterNextRender를 사용합니다:
import { afterNextRender } from '@angular/core';
export class MyComponent {
constructor() {
afterNextRender(() => {
// 이 코드는 브라우저에서 렌더링 후에만 실행됩니다.
document.title = 'My Page';
});
}
}
수화 불일치 해결
수화 불일치 진단을 활성화합니다:
import { provideClientHydration, withDomHydrationErrorRecovery } from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [
provideClientHydration(
withDomHydrationErrorRecovery()
)
]
});
일반적인 수화 불일치 원인:
- 동적 텍스트 콘텐츠: 서버와 클라이언트에서 다른
Date,Math.random()값이 생성됨.afterNextRender사용. - 브라우저 전용 스타일:
:host-context와 같은 CSS 클래스가 서버에서 다르게 적용됨. - 서드파티 라이브러리: DOM을 직접 조작하는 라이브러리.
isPlatformBrowser()로 감싸기. - 주입된 HTML:
innerHTML로 설정된 콘텐츠가 서버와 클라이언트에서 다름.
정적 페이지 사전 렌더링
angular.json에서 사전 렌더링할 라우트를 지정합니다:
"prerender": {
"routes": [
"/",
"/products",
"/about"
]
}
사전 렌더링을 실행합니다:
ng build
SSR 호환 코드 작성 모범 사례
localStorage,sessionStorage사용 금지:afterNextRender또는isPlatformBrowser()로 보호.window,document참조 금지: 서버에서 정의되지 않음.afterNextRender사용.ElementRef의 nativeElement 속성: 서버에서는 null일 수 있으므로 조건부 접근.- 동적 데이터를 생성할 때
TransferState사용: 서버에서 클라이언트로 데이터 전달.
import { TransferState, makeStateKey } from '@angular/core';
const COUNTER_KEY = makeStateKey<number>('counter');
@Component({
// ...
})
export class CounterComponent {
private transferState = inject(TransferState);
private platformId = inject(PLATFORM_ID);
counter: number;
constructor() {
if (isPlatformServer(this.platformId)) {
this.counter = 42; // 서버에서 생성
this.transferState.set(COUNTER_KEY, this.counter);
} else {
this.counter = this.transferState.get(COUNTER_KEY, 0); // 클라이언트에서 읽기
}
}
}