이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.
이 게시물의 소스코드는 검색 엔진 최적화(SEO) / Angular Universal로 SEO 적용하기 (상)에서 이어집니다.
angular-site.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.
angular-site.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 angular-site.git 을 clone 합니다.
- Github에서 소스코드 보기: https://github.com/a-mean-blogger/angular-site/tree/5c54369d90f34f5f2b5e7d563e0575f4dcfa9bf4
전편에서는 Angular Universal을 사용하기 위해 browser 코드들을 정리하였습니다. 이번에는 실제로 Angular Universal을 적용해 보겠습니다.
아래 명령어로 @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를 하는 방법에 대해 알아보겠습니다.
댓글
이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기