Scully로 SSG 구현하기 (Creating SSG with Scully)
과제
Scully를 적용하였을 때, 아래의 과제를 테스트 해봅니다.
- 빌드 타임에 모든 형태의 routing 별로 index.html 이 만들어지는가?
- 공통 meta, title 설정이 가능한가?
- 만일 그렇다면 routing별로 각각 다른 dynamic meta, title 설정이 가능한가?
계획
title과 meta 설정 테스트를 다음과 같이 진행 합니다.
- firstpage: lazyloading 적용, routing module에서 설정한 값을 component에서 title과 meta 적용
- secondpage: fastloading 적용, component에서 직접 title, meta적용
- thirdpage: 값을 json설정 값으로 미리 만들어 두고, routing이 변경될 때 이를 불러와서 title과 meta 적용 및 component 에서의 설정과 routing에서의 설정이 중복되었을 때 반응 테스트
DEMO 작성
프로젝트를 생성하고, 고정 title 및 meta를 적용합니다.
Angular 프로젝트를 생성하고 라우팅을 설정 합니다.
Routing은 아래와 같이 작성하였습니다.
// <app-routing.module.ts>
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SendPageComponent } from './secondpage/secondpage.component';
const routes: Routes = [
{ path: 'firstpage', loadChildren: () => import('./firstpage/firstpage.module').then(m => m.FirstpageModule), data: {title: 'first'} }, // firstpage 테스트용.
{ path: 'secondpage', component: SecondPageComponent}, // secondpage 테스트용
{ path: 'thirdpage', loadChildren: () => import('./thirdpage/thirdpage.module').then(m => m.ThirdpageModule)}, // thirdpage 테스트용
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Scully 설치
angular 프로젝트에 scully를 설치합니다.
ng add @scullyio/init
scully가 설치되면 app.module에 ScullyLibModule이 추가되며,
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ScullyLibModule } from '@scullyio/ng-lib';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
ScullyLibModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
package.json에 scully 관련 script가 추가됩니다.
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"scully": "scully",
"scully:serve": "scully serve"
},
마지막으로 scully.[project].config.ts가 생성됩니다. 이는 몇가지 설정이 포함되어 있으며, query data를 반영해야 할 때 활용할 수 있습니다. 자세항 사항은 아래 참고 사이트를 참고 하시기 바랍니다.
import { ScullyConfig } from '@scullyio/scully';
export const config: ScullyConfig = {
projectRoot: "./src",
projectName: "scully-sample",
outDir: './dist/static',
routes: {
}
};
Scully 빌드
Scully는 빌드 시점에 라우팅에 따라 index를 생성합니다. 따라서 프로젝트가 먼저 빌드 되어야 scully가 적용됩니다.
ng build --prod && npm run scully
실행하면, scully가 모든 routing을 찾고 index.html을 생성하는 로그를 확인할 수 있습니다.
...
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
설정값을 수정하지 않는다면, dist/static에 빌드 파일이 추가되며, 확인해보면 각 route에 index.html이 성공적으로 생성되어 있는 것을 확인할 수 있습니다.
기본적으로 scully는 title과 meta 값의 설정이 없으면 src/index.html의 설정값을 모든 index.html에 적용합니다.
각 index.html을 열어 적용했던 Title과 Meta가 올바르게 적용되었는지 확인합니다.
가변 Title, Meta 적용
@angular/platform-browser 의 Meta와 title을 활용합니다.
import { Meta, Title } from '@angular/platform-browser';
...
constructor(private title: Title, private meta: Meta) {
this.title.setTitle('');
this.meta.addTag(); // updateTag, removeTag
}
모든 component에서 매번 적용하는 귀찮음을 피하기 위해 service를 작성합니다.
// <SEOservice.ts>
import {Injectable} from '@angular/core';
import { Meta, Title, MetaDefinition } from '@angular/platform-browser';
@Injectable({ providedIn: 'root' })
export class SEOService {
constructor(private title: Title,
private meta: Meta
) {
}
updateTitle(title?: string): void {
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 });
}
}
firstpage.component에는 app.routing.module 에서 설정한 값을 가져와서 적용합니다.
// <firstpage.component.ts>
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { SEOService } from 'src/app/service/seo.service';
@Component({
selector: 'app-firstpage',
templateUrl: './firstpage.component.html',
styleUrls: ['./firstpage.component.scss']
})
export class FirstpageComponent implements OnInit {
constructor(
private seoService: SEOService,
private activatedRoute: ActivatedRoute
) { }
ngOnInit(): void {
this.seoService.updateTitle(this.activatedRoute.snapshot.firstChild.data?.title);
}
}
secondpage.component에는 직접 title과 meta 정보를 설정합니다.
//<secondpage.component.ts>
import { Component, OnInit } from '@angular/core';
import { SEOService } from 'src/app/service/seo.service';
@Component({
selector: 'app-secondpage',
templateUrl: './secondpage.component.html',
styleUrls: ['./secondpage.component.scss']
})
export class SecondpageComponent implements OnInit {
constructor(
private seoService: SEOService
) { }
ngOnInit(): void {
this.seoService.updateTitle('2pageTitle');
this.seoService.updateOgDescription('second description');
}
}
다음 테스트를 위해 json 파일을 생성합니다. 라우팅 구성과 동일한 키를 사용하여 설정해보았습니다.
// <metaData.json>
{
"thirdpage": {
"property": "og:description",
"content": "second description",
"name": "description"
}
}
SEOService에서 라우팅에 따라 자동으로 알맞은 json을 가져올 수 있도록 코드를 수정합니다.
// <seo.service.ts>
// <생략>
...
getMeta(url: string): any {
// 주소값을 받아 주소값 내의 meta정보를 가져옴.
return metaInfo[url];
}
setMeta(data: MetaDefinition) {
if (!data) {
return;
}
this.meta.updateTag(data);
}
app.component에서 routing을 감지하여 현 url을 seoService로 넘기는 코드를 작성합니다.
보통 라우팅 감지는 Router와 ActivedRoute로 감지하지만, 여기에서는 Scully에서 제공하는 ScullyRoutesService를 활용합니다.
(참고로, scully가 감지한 모든 route를 확인하려면 available$를. 현재 url을 확인하려면 getCurrent()를 활용합니다.)
//<app.component.ts>
import { OnInit, Component } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';
import { SEOService } from './service/seo.service';
import * as MetaData from './assets/metaData.json';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'scully-sample';
// allLinks$ = this.scully.available$;
link$ = this.scully.getCurrent();
constructor(
private scully: ScullyRoutesService,
private seoService: SEOService
) {
}
ngOnInit() {
this.link$.subscribe(links => {
const info = this.seoService.getMeta(links.route);
this.seoService.updateTitle(links.route);
this.seoService.setMeta(info);
});
}
다음은 thirdpage.component의 onInit에 secondpage.component와 같이 강제로 Meta를 설정하는 코드를 작성합니다.
이것으로 routing에 의해 설정된 title과 meta가 적용되는지 아니면 component에서 설정한 title과 meta가 적용되는지 확인할 수 있습니다.
// <secondpage.component.ts>
import { Component, OnInit } from '@angular/core';
import { SEOService } from 'src/app/service/seo.service';
@Component({
selector: 'app-thirdpage',
templateUrl: './thirdpage.component.html',
styleUrls: ['./thirdpage.component.scss']
})
export class ThirdpageComponent implements OnInit {
constructor(
private seoService: SEOService
) { }
ngOnInit(): void {
this.seoService.updateTitle('3pageTitle');
this.seoService.updateOgDescription('third description');
}
}
이 테스트의 결과는 thirdpage component의 ngOnInit에서 설정된 값이 반영되는 것을 확인할 수 있습니다.
scully의 getCurrent()가 보다 늦게 호출되기 때문에 여기에서 설정한 값을 덮어쓰게 되기 때문입니다.
결론
계획했던 테스트의 결과는 다음과 같습니다.
- firstpage: routing module에서 설정한 값을 component에서 적용 (성공)
- secondpage: 이미 로딩되어 있는 component에서 적용 (성공)
- thirdpage: json설정 값을 routing이 변경될 때 불러와서 적용(성공)
- thirdpage: component 에서의 설정과 routing에서의 설정 중복 테스트 (component의 설정이 적용)
남은 과제
naver나 daum등 국내 포탈에서 실제로 적용되는가?
댓글남기기