Scully로 SSG 구현하기

Adam Kim·2025년 10월 10일
0

angular

목록 보기
45/88

과제

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은 아래와 같이 작성하였습니다.


// 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)
  ]
};

Scully 설치

angular 프로젝트에 scully를 설치합니다.

ng add @scullyio/init


Scully가 설치되면 app.config.ts에 provideScully가 추가될 수 있습니다. (만약 자동으로 추가되지 않는다면 수동으로 추가해야 합니다.)

// 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 설정 추가
  ]
};

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';
import { inject } from '@angular/core';

// 컴포넌트 내부에서 DI
const titleService = inject(Title);
const metaService = inject(Meta);

titleService.setTitle('');
metaService.addTag({ name: 'description', content: '...' }); // updateTag, removeTag도 사용 가능

모든 component에서 매번 적용하는 귀찮음을 피하기 위해 service를 작성합니다.

// 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 });
  }
}

firstpage.component에는 app.routing.module 에서 설정한 값을 가져와서 적용합니다.

// 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']);
  }
}

secondpage.component에는 직접 title과 meta 정보를 설정합니다.

// 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');
  }
}

다음 테스트를 위해 json 파일을 생성합니다. 라우팅 구성과 동일한 키를 사용하여 설정해보았습니다.


// <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);
    }
  }
}

app.component에서 routing을 감지하여 현 url을 seoService로 넘기는 코드를 작성합니다.

보통 라우팅 감지는 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);
      }
    });
  }
}

다음은 thirdpage.component의 onInit에 secondpage.component와 같이 강제로 Meta를 설정하는 코드를 작성합니다.
이것으로 routing에 의해 설정된 title과 meta가 적용되는지 아니면 component에서 설정한 title과 meta가 적용되는지 확인할 수 있습니다.

// 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');
  }
}

이 테스트의 결과는 thirdpage component의 ngOnInit에서 설정된 값이 반영되는 것을 확인할 수 있습니다.
scully의 getCurrent()가 보다 늦게 호출되기 때문에 여기에서 설정한 값을 덮어쓰게 되기 때문입니다.

결론

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



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

남은 과제

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

참고 사이트

profile
Angular2+ Developer

0개의 댓글