사이트 호스팅을 제공하는 회사(shopify, 카페24, 아임웹 등) 특성상 제공 받은 업체의 사이트를 SSR로 서빙할 때, 적절한 메타 태그와 구조화된 데이터를 삽입하는 것은 중요하다.
SEO 측면은 말할 것도 없고, 제대로 설정해두지 않으면 다음과 같은 문제가 발생할 수 있다.
우리 회사 이름을 딩딩, 호스팅 제공받는 업체 이름을 탐조월드라고 가정,
구글에 탐조월드를 검색했으나, 아래처럼 우리 회사 이름이 뜰 수도 있다..

그래서 구조화 데이터를 추가하여 이를 보강하는 작업을 해주다 문제가 발생했다.
async start(){
/************** 새로 추가한 로직 ****************/
// 사이트 관련 정보를 fetch하고, 구조화 데이터를 삽입한다.
const {siteName, siteUrl} = await this._retrieveSiteFullUrl();
this._updateSchemaOrgWebsite({ siteName, siteUrl });
//--------------------------------------------/
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe((event) => {
void 메타태그삽입(e.url);
}
}
/**
* document body에 마이크로데이터(구조화 데이터) 추가하는 로직, 간소화함
*/
private _updateSchemaOrgWebsite({ siteName, siteUrl }: { siteName: string; siteUrl: string }): void {
const schemaDiv = ...
const body = this.renderer.selectRootElement('body', true);
this.renderer.appendChild(body, schemaDiv);
}
위와 같이 코드를 짜니까, 구조화 데이터는 추가가 되는데 메타 태그 삽입이 안됐다.
처음에는 문제가 뭐지? renderer에 문제가 있나? 싶었는데 아니었다.
문제는 간단한 거였는데..사수님이 알려주시기 전까지 정확히 문제가 뭔지도 몰랐다.
문제는 await this._retrieveSiteFullUrl(); 호출 시점에 있었다.
async start(){
// 여기서 await을 소비하는 동안 NavigationEnd 이벤트가 일어남!
const {siteName, siteUrl} = await this._retrieveSiteFullUrl();
this._updateSchemaOrgWebsite({ siteName, siteUrl });
// NavigationEnd 이벤트가 이미 일어난 이후 구독을 시작해서,
// 메타 태그 삽입이 이루어지지 않음
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe((event) => {
void 메타태그삽입(e.url);
}
}
정리하면,
await this._retrieveSiteFullUrl() 하는 동안 첫번째 NavigationEnd가 일어났다.NavigationEnd 이벤트를 감지하는 방법도 딱히 없었기 때문에 처음에 메타 태그 삽입 로직이 아예 일어나지 않는다.알고나면 되게 기본적이고 간단한 것인데... 문제를 너무 어렵게 본 것 같다.
그럼 해결은 어떻게 할까?
async start(){
// 항상 최상위에
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe((event) => {
void 메타태그삽입(e.url);
}
await 어쩌구();
}
문제점 : 혹여혹여혹여혹여혹여나 subscription 이전에 꼭 실행해야 하는 작업이 있으면 어떡할 것인가?
startsWith는 선언 시점에 무조건 값을 하나 내뱉어준다.
async start(){
await 어쩌구(); // NavigationEnd 발생!
this.router.events
.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
startWith({ url: this.router.url, urlAfterRedirects: this.router.url } as NavigationEnd),
// 현재 url로 값을 무조건 뱉으면, 이미 일어난 걸 감지 못하더라도 올바른 url을 획득 가능
)
.subscribe((e) => {
void 메타태그삽입(e.url);
});
}
문제점: 처음 발생하는 NavigationEnd를 잘 감지하는 케이스의 경우에는 this.router.url이 비어 있기 때문에 메타 태그에 이상한 값이 삽입될 수 있다. 그러나 바로 NavigationEnd가 일어나니 크게 문제는 없다. 그러나 찜찜해. . . .
private navigationEndReplaySubject = new ReplaySubject<NavigationEnd>(1);
// 1은 버퍼 사이즈, 저장할 이벤트 수
constructor(...) {
// constructing 시점부터 NavigationEnd 구독하여 저장
this.router.events
.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
.subscribe((e) => this.navigationEndReplaySubject.next(e));
}
async start(){
await 어쩌구();
// 여기서 구독을 이어가면, 이전 이벤트가 일어나더라도 버퍼에 남아있기 때문에 놓치는 값 없이 수행 가능!
this.navigationEndReplaySubject.subscribe((e) => {
void 메타태그삽입(e.url);
});
}
async start() {
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe(async (event) => {
if (!중복) {
await 어쩌구();
중복 = true;
}
void 메타태그삽입(e.url);
}
}
방법은 또 더 많겠지만 이것저것 알아본 건 여기까지..
요즘은 AI 코드 리뷰를 하는 일이 잦다보니, 코드를 읽고 생각하는 집중력이 떨어지는 것 같다. 차근히 코드의 흐름을 잘 살펴보자. 늘 기본에 충실히...
도움 많이 된다