Scully를 적용하였을 때, 아래의 과제를 테스트 해봅니다.
1. 빌드 타임에 모든 형태의 routing 별로 index.html 이 만들어지는가?
2. 공통 meta, title 설정이 가능한가?
3. 만일 그렇다면 routing별로 각각 다른 dynamic meta, title 설정이 가능한가?
title과 meta 설정 테스트를 다음과 같이 진행 합니다.
프로젝트를 생성하고, 고정 title 및 meta를 적용합니다.
Angular 프로젝트를 생성하고 라우팅을 설정 합니다.
Routing은 아래와 같이 작성하였습니다.
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { SecondPageComponent } from './secondpage/secondpage.component';
export const routes: Routes = [
{
path: 'firstpage',
loadComponent: () => import('./firstpage/firstpage.component').then(c => c.FirstpageComponent),
data: { title: 'first' } // firstpage 테스트용 데이터
},
{
path: 'secondpage',
component: SecondPageComponent // secondpage 테스트용
},
{
path: 'thirdpage',
loadComponent: () => import('./thirdpage/thirdpage.component').then(c => c.ThirdpageComponent) // thirdpage 테스트용
},
];
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes)
]
};
angular 프로젝트에 scully를 설치합니다.
ng add @scullyio/init
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideScully } from '@scullyio/ng-lib';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideScully() // Scully 설정 추가
]
};
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"scully": "scully",
"scully:serve": "scully serve"
},
import { ScullyConfig } from '@scullyio/scully';
export const config: ScullyConfig = {
projectRoot: "./src",
projectName: "scully-sample",
outDir: './dist/static',
routes: {
}
};
Scully는 빌드 시점에 라우팅에 따라 index를 생성합니다. 따라서 프로젝트가 먼저 빌드 되어야 scully가 적용됩니다.
ng build --prod && npm run scully
...
Route "/firstpage" rendered into file: "./dist/static/firstpage/index.html"
Route "/secondpage" rendered into file: "./dist/static/secondpage/index.html"
Route "/thirdpage" rendered into file: "./dist/static/thirdpage/index.html"
Route "/" rendered into file: "./dist/static/index.html"
Generating took 3.74 seconds for 4 pages:
That is 2.67 pages per second,
or 375 milliseconds for each page.
Finding routes in the angular app took 3 milliseconds
Pulling in route-data took 0 milliseconds
Rendering the pages took 2.91 seconds
@angular/platform-browser 의 Meta와 title을 활용합니다.
import { Meta, Title } from '@angular/platform-browser';
import { inject } from '@angular/core';
// 컴포넌트 내부에서 DI
const titleService = inject(Title);
const metaService = inject(Meta);
titleService.setTitle('');
metaService.addTag({ name: 'description', content: '...' }); // updateTag, removeTag도 사용 가능
// src/app/seo.service.ts
import { Injectable, inject } from '@angular/core';
import { Meta, Title, MetaDefinition } from '@angular/platform-browser';
@Injectable({ providedIn: 'root' })
export class SEOService {
private title = inject(Title);
private meta = inject(Meta);
updateTitle(title?: string): void {
if (title) {
this.title.setTitle(title);
}
}
updateOgDescription(desc: string): void {
this.meta.updateTag({ property: 'og:description', content: desc });
}
updateDescription(desc: string): void {
this.meta.updateTag({ name: 'description', content: desc });
}
}
// src/app/firstpage/firstpage.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { SEOService } from '../seo.service';
@Component({
selector: 'app-firstpage',
standalone: true,
template: `<p>firstpage works!</p>`,
})
export class FirstpageComponent implements OnInit {
private seoService = inject(SEOService);
private activatedRoute = inject(ActivatedRoute);
ngOnInit(): void {
const routeData = this.activatedRoute.snapshot.data;
this.seoService.updateTitle(routeData['title']);
}
}
// src/app/secondpage/secondpage.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { SEOService } from '../seo.service';
@Component({
selector: 'app-secondpage',
standalone: true,
template: `<p>secondpage works!</p>`,
})
export class SecondpageComponent implements OnInit {
private seoService = inject(SEOService);
ngOnInit(): void {
this.seoService.updateTitle('2pageTitle');
this.seoService.updateOgDescription('second description');
}
}
// <metaData.json>
{
"thirdpage": {
"property": "og:description",
"content": "second description",
"name": "description"
}
}
SEOService에서 라우팅에 따라 자동으로 알맞은 json을 가져올 수 있도록 코드를 수정합니다.
// src/app/seo.service.ts
// ... (기존 코드 생략)
import metaInfo from '../assets/metaData.json';
@Injectable({ providedIn: 'root' })
export class SEOService {
// ... (기존 코드 생략)
getMeta(url: string): MetaDefinition | undefined {
// metaInfo의 타입을 명확하게 지정하거나 as any를 사용
return (metaInfo as any)[url];
}
setMeta(data?: MetaDefinition): void {
if (data) {
this.meta.updateTag(data);
}
}
}
보통 라우팅 감지는 Router와 ActivedRoute로 감지하지만, 여기에서는 Scully에서 제공하는 ScullyRoutesService를 활용합니다.
(참고로, scully가 감지한 모든 route를 확인하려면 available$를. 현재 url을 확인하려면 getCurrent()를 활용합니다.)
// src/app/app.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ScullyRoutesService } from '@scullyio/ng-lib';
import { SEOService } from './seo.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `<router-outlet></router-outlet>`,
})
export class AppComponent implements OnInit {
title = 'scully-sample';
private scully = inject(ScullyRoutesService);
private seoService = inject(SEOService);
// 현재 라우트 정보를 담는 Signal
currentLink$ = this.scully.getCurrent();
ngOnInit() {
this.currentLink$.subscribe(link => {
// route가 '/'가 아닐 때만 title, meta 설정
if (link.route && link.route !== '/') {
const metaData = this.seoService.getMeta(link.route);
this.seoService.updateTitle(link.title);
this.seoService.setMeta(metaData);
}
});
}
}
// src/app/thirdpage/thirdpage.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { SEOService } from '../seo.service';
@Component({
selector: 'app-thirdpage',
standalone: true,
template: `<p>thirdpage works!</p>`,
})
export class ThirdpageComponent implements OnInit {
private seoService = inject(SEOService);
ngOnInit(): void {
this.seoService.updateTitle('3pageTitle from Component');
this.seoService.updateOgDescription('third description from Component');
}
}
계획했던 테스트의 결과는 다음과 같습니다.
1. firstpage: routing module에서 설정한 값을 component에서 적용 (성공)
2. secondpage: 이미 로딩되어 있는 component에서 적용 (성공)
3. thirdpage: json설정 값을 routing이 변경될 때 불러와서 적용(성공)
4. thirdpage: component 에서의 설정과 routing에서의 설정 중복 테스트 (component의 설정이 적용)
naver나 daum등 국내 포탈에서 실제로 적용되는가?