Scully로 SSG 구현하기 (Creating SSG with Scully)

5 분 소요

과제

Scully를 적용하였을 때, 아래의 과제를 테스트 해봅니다.

  1. 빌드 타임에 모든 형태의 routing 별로 index.html 이 만들어지는가?
  2. 공통 meta, title 설정이 가능한가?
  3. 만일 그렇다면 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()가 보다 늦게 호출되기 때문에 여기에서 설정한 값을 덮어쓰게 되기 때문입니다.

결론

계획했던 테스트의 결과는 다음과 같습니다.

  1. firstpage: routing module에서 설정한 값을 component에서 적용 (성공)
  2. secondpage: 이미 로딩되어 있는 component에서 적용 (성공)
  3. thirdpage: json설정 값을 routing이 변경될 때 불러와서 적용(성공)
  4. thirdpage: component 에서의 설정과 routing에서의 설정 중복 테스트 (component의 설정이 적용)

남은 과제

naver나 daum등 국내 포탈에서 실제로 적용되는가?

참고 사이트

댓글남기기