Angular Universal로 SEO 적용하기 (하)

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 검색 엔진 최적화(SEO) / Angular Universal로 SEO 적용하기 (상)에서 이어집니다.

angular-site.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard 5c54369
git reset --soft 3733c5b
npm install
atom .

angular-site.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 angular-site.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/angular-site.git
cd angular-site
git reset --hard 5c54369
git reset --soft 3733c5b
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/angular-site/tree/5c54369d90f34f5f2b5e7d563e0575f4dcfa9bf4


전편에서는 Angular Universal을 사용하기 위해 browser 코드들을 정리하였습니다. 이번에는 실제로 Angular Universal을 적용해 보겠습니다.

Package 설치

아래 명령어로 @angular/platform-server, @nguniversal/express-engine 의 2개의 package를 설치해 줍니다.

$ npm install @angular/platform-server @nguniversal/express-engine --save

폴더 구조

주황색은 변경된 파일, 녹색은 새로 생성된 파일, 회색은 변화가 없는 파일입니다.

코드

app.module.ts부터 살펴봅시다.

// src/app/app.module.ts (수정)

//...생략

@NgModule({
  //...생략
  imports: [
    BrowserModule.withServerTransition({appId: 'my-app-id'}), //1
    AppRoutingModule,
    //...생략
  ],
  //...생략
})
export class AppModule { }

1. BrowserModule를 위와 같이 변경시켜줍니다. appId는 아무거나 넣어줍니다.

// src/app/app.server.module.ts (새 파일)

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    // The AppServerModule should import your AppModule followed
    // by the ServerModule from @angular/platform-server.
    AppModule,
    ServerModule,
  ],
  // Since the bootstrapped component is not inherited from your
  // imported AppModule, it needs to be repeated here.
  bootstrap: [AppComponent],
})
export class AppServerModule {}

app.server.module.ts는 서버용 app을 위한 module입니다. 클라이언트용 app의 AppMoudle과 ServerModule을 import한 다음에 export하는 것을 볼 수 있습니다.

// src/main.server.ts (새 파일)

import { environment } from './environments/environment';
import { enableProdMode } from '@angular/core';

if (environment.production) {
  enableProdMode();
}

export {AppServerModule} from './app/app.server.module';

main.server.ts는 서버용 app에서 가장 main이 되는 ts파일입니다(클라이언트용 app의 main.ts 역할). 바로 위에서 만든 AppServerModule을 export하고 있습니다.

// src/tsconfig.server.json (새 파일)

{
  "extends": "./tsconfig.app.json", //1
  "compilerOptions": {
    "outDir": "../out-tsc/server",
    "module": "commonjs"
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule" //2
  }
}

tsconfig.server.json은 서버용 app의 TypeScript setting 파일입니다(클라이언트용 app의 tsconfig.app.json의 역할). tsconfig.app.json을 extends하고 있는 것을 볼 수 있고(//1), 위에서 만든 AppServerModule을 entry module로 지정한 것을 볼 수 있습니다(//2).

// .angular-cli.json

{
  //...생략
  "apps": [
    {
      //...생략
    },
    {
       "name": "server-app", //1
       "platform":"server",
       "root":"src",
       "outDir":"dist-server", //2
       "assets":[
          "assets",
          "favicon.ico"
       ],
       "index":"index.html",
       "main":"main.server.ts", //3
       "test":"test.ts",
       "tsconfig":"tsconfig.server.json", //4
       "testTsconfig":"tsconfig.spec.json",
       "prefix":"app",
       "styles":[
          "styles.css"
       ],
       "scripts":[

       ],
       "environmentSource":"environments/environment.ts",
       "environments":{
          "dev":"environments/environment.ts",
          "prod":"environments/environment.prod.ts"
       }
    }
  ],
  //...생략
}

.angular-cli.json의 apps 항목을 보면 기존의 angular app에 대한 setting들이 있는데, 여기에 서버용 app에 대한 setting을 추가해 줍니다.

1. name을 "server-app"으로 하였습니다. 이름, 또는 apps 항목의 순서로 어떤 app을 build할 것인지 구별을 할 수 있게 됩니다.
2. build시 결과물이 나오는 폴더 위치입니다. 이 위치(/dist-server)를.gitignore에 추가해 줍니다.
3&4. 위에서 만든 main.server.ts와 tsconfig.server.json이 지정됩니다.

// server/server.js (새 파일)

require('zone.js/dist/zone-node');
require('reflect-metadata');

var express     = require('express');
var path        = require('path');
var ngUniversal = require('@nguniversal/express-engine');
var appServer   = require('../dist-server/main.bundle'); //1

var app = express();

app.engine('html', ngUniversal.ngExpressEngine({
  bootstrap: appServer.AppServerModuleNgFactory
}));

app.set('view engine', 'html');
app.set('views', path.join(__dirname, '../dist'));
app.use('/', express.static(path.join(__dirname, '../dist'), {index: false}));

app.get('*', function(req, res){
  res.render('index', { //2
    req: req,
    res: res,
  });
});

var port = 4000; //3
app.listen(port, function(){
  console.log('listening on http://127.0.0.1:'+port+'/');
});

Node JS Express 서버파일입니다. 

1. build된 서버용 app을 불러오는 부분입니다.
2. build된 클라이언트용 app을 render하는 부분입니다.
3. Angular 2/기본사이트 만들기 사이트는 Node JS API/JWT(JSON Web Token)로 로그인 REST API 만들기의 API가 필요한데, 이 API가 3000번 port에서 실행되기 때문에 Angular Universal 서버는 4000으로 하였습니다.

마지막으로 package.json에서 script들을 수정해 줍시다.

// package.json

{
  //...생략
  "scripts": {
    "ng": "ng",
    "start": "node server/server", //1
    "build": "npm run build:client && npm run build:server", //2
    "build:server": "ng build --prod --app server-app --output-hashing=false", //3
    "build:client": "ng build --prod", //4
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
 //...생략
}

1. 서버의 실행을 하는 스크립트입니다.

2. build 스크립트는 서버용 app과 클라이언트용 app을 모두 build할 때 사용되는 스크립트입니다. 아래의 build:server 스크립트와 build:client 스크립트을 호출합니다.

3. build:server는 서버용 app을 build할때 사용되는 스크립트입니다. --app server-app 옵션으로 .위에서 만든 angular-cli.json의 apps의 server-app의 설정을 사용하게 됩니다. --output-hashing=false 옵션은 build시에 파일이름에 hash값을 넣지 않습니다. build를 한 후에 이 옵션이 없는 클라이언트용 app의 build out 폴더(./dist)를 보면 main.15fc3843b01d28db551b.bundle.js과 같이 파일이름에 hash가 들어가 있는데, 서버용 app의 build out 폴더(./dist-server)를 보면 main.bundle.js와 같이 hash가 없게 됩니다. 우리가 만든 server.js파일에서 이 서버용 main.bundle.js을 읽어와야 되는데, hash때문에 이름이 바뀌면 안되기 때문에 이 옵션이 들어갑니다.

4. build:client는 클라이언트용 app을 build할때 사용되는 스크립트입니다.

실행 결과

아래 명령어로 app을 build해 줍니다.

$ npm run build

우리가 만든 package.json의 build 스크립트가 실행되며 dist, dist-server 폴더가 생성되고 build파일이 생성됩니다.

빌드가 모두 끝나면 nodemon을 사용해서 서버를 실행합시다

nodemon

웹사이트가 브라우저에 실행되면 view-source를 봅시다.

사이트의 컨탠츠가 서버에서 생성되어 온 것을 볼 수 있습니다.

마치며..

이제 Angular 사이트로 SEO를 할 수 있는 준비가 되었습니다. 즉, 검색엔진이 사이트의 컨탠츠를 읽을 수 있는 사이트가 된 것으로 실제 SEO를 하는 것은 전혀 다른 주제입니다. 다음에는 Angular Universal로 정말 기초적인 SEO를 하는 방법에 대해 알아보겠습니다.

댓글

주난 2018.04.05
안녕하세요. mean stack 에 대해서 관심있게 보고 있습니다. 다른것들보다 angular universal 에서 디버깅이 안되서 고생하고 있습니다. 혹시 디버깅 하는 방법을 알고 계시다면 도움 말씀 부탁드립니다.
I
Ian H 2018.04.06
@주난,
안녕하세요 debugger 를 쓰시면 됩니다. 음.. 말로 설명하는 것 보다  https://www.youtube.com/watch?v=1BSrE-OUXm4 를 보시면 바로 이해가 되실거에요^^
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP