Node.js

여기서는 노드와 익스프레스 웹서버에 관해 살펴봅니다

자바스크립트는 웹문서에 동적인 움직임을 주기위해 만들어진 프로그래밍 언어로서, 자바스크립트로 작성한 프로그램이 스크립트인데, 브라우저에 내장된 자바스크립트 엔진은 스크립트 코드를 읽어들여서 분석하고(= 파싱), 컴퓨터가 알 수 있는 기계어로 번역하여(= 컴파일), 그 코드 내용을 실행해나가게 된다!

자바스크립트는 많이 어렵습니다. 순서대로만 설명할 수도 없고, 그렇게 배워나갈 수도 없습니다. 지나갔다, 다시 돌아오고,, 다시 나아가고,,, 하면서 수없는 반복을 통해 익혀나가야 합니다 ㅡㅡ; 러시아 혁명가 레닌이 쓴 유명한 팜플렛 [1보 전진 2보 후퇴](= 한걸음 나아가기 위한 2보 후퇴)가 생각나는군요 혹시나 이 말에 의문을 품는 분이 계실까 해서 붙이는데.. [1보 후퇴 for 2보 전진]이 아닙니다 ^^

노드 시작하기

Node의 모든 작업은 각각의 프로젝트별 루트 디렉토리 를 만들어서 수행하며, 모든 경로 지정은 이 루트 디렉토리를 기준으로 한다!

노드 npm
Node를 설치하면( ) npm도 함께 설치되고, 이어서 작업을 위한 프로젝트 루트 폴더(예컨대, ex1 폴더)에서 npm init -y(= 기본값으로 초기화) 명령으로 프로젝트 초기화 작업을 수행해준다. 이제 프로젝트 루트에 아래와 같은 package.json 파일이 생성된다:
[ 기본값으로 생성된 package.json ]
                                        
                                            /* ./package.json */
                                            {
                                                "name": "ex1", // ← 프로젝트 루트 폴더
                                                "version": "1.0.0",
                                                "main": "index.js", // ← 프로젝트 진입 파일
                                                "scripts": {
                                                    "test": "echo \"Error: no test specified\" && exit 1"
                                                },
                                                "keywords": [],
                                                "author": "",
                                                "license": "ISC",
                                                "description": ""
                                            }
                                        
                                    

이 JSON 파일은 자바스크립트 애플리케이션의 진입점이자 외부 의존성에 대한 정보를 저장하는 곳인데, 내부 메타 데이터들은 언제든 수정 가능하다!

➥ Windows Powershell

Node를 사용하려면; 먼저 [Windows Powershell]을 실행하여 커맨드 쉘 로 들어가야 한다: 예컨대, 작업하려는 특정 프로젝트 루트 폴더에서 윈도우 탐색창의 Shift+우측버튼을 누른 뒤, <여기에 PowerShell 창 열기>


✓   콘솔 터미널에서 스크립트 파일을 실행할 때는 node my-js.js, 콘솔에서 노드로 들어가려면; node, 콘솔에서 노드의 패키지 매니저인 npm(노드 설치 시 함께 설치된다)을 새로 설치할 때는; 해당 프로젝트 폴더에서 npm install(npm을 초기화하면서 패키지 의존 관계는 남기고자 한다면; npm init -y) 명령을 수행해주면 된다

1. NPM 패키지는 npm install lodash 식으로 설치하며(* 제거는, npm uninstall lodash), 설치/제거 시 -g(터미널 차원에서의 전역 설치) 옵션을 줄 수도 있다:
                                    
                                        /* [lodash] 설치 이후 변경된 ./package.json */
                                        {
                                            "name": "ex1",
                                            "version": "1.0.0",
                                            "main": "index.js",
                                            "scripts": {
                                                "test": "echo \"Error: no test specified\" && exit 1"
                                            },
                                            "keywords": [],
                                            "author": "",
                                            "license": "ISC",
                                            "description": "",
                                            "dependencies": { "lodash": "^4.17.21" } // ← 설치한 lodash 패키지
                                        }
                                    
                                

로대시 설치 이후 프로젝트 루트 폴더에 가보면; package.json 파일만 아니라 package-lock.json 파일도 생성되어 있다!

2. 설치한 로대시 라이브러리는 다음과 같이 스크립트에서 사용할 수 있다:
                                    
                                        // npm으로 로대시를 설치했기에 따로 경로를 적을 필요는 없다!
                                        import lodash, { fromPairs } from 'lodash' // 로대시의 기본 객체 lodash와 개별 함수 { fromPairs } 임포트
                                        
                                        export function mapToObject(map) { // 로대시 함수 익스포트
                                            return fromPairs([...map]);
                                        }
                                        
                                        export function objectToMap(object) { // 로대시 함수 익스포트
                                            const pairs= lodash.toPairs(object)
                                            return new Map(pairs);
                                        }
                                    
                                

상대 경로를 사용하지 않는 임포트는 외부에서 불러온 코드인데, 그 세부 의존성 정보는 package-lock.json 파일에 저장된다!


✓   package-lock.json 파일은 설치 시점 당시의 의존성 정보를 저장하는데, 이는 npm을 새로 설치할 때도 이전에 설치된 패키지들은 당시와 동일한 버전으로 설치할 수 있도록 해준다!

노드 모듈 작성 및 내보내기, 가져오기
노드는 require() 함수를 통해 다른 모듈을 가져오고, Exports 객체로 공개 API를 내보낸다. 노드의 전역 객체 exports는 항상 정의되어 있고, 여러가지 값을 내보내는 노드 모듈을 만들 때, 이 객체의 프로퍼티로 할당해주면 된다:
[ 노드 모듈 작성 및 내보내기: exports ]
                                        
                                            // 비공개 함수 작성: 캡슐화
                                            const sum= (x, y) => x + y
                                            const square= x => x * x
                                            
                                            // 공개할 함수 작성: 모듈화
                                            exports.mean= (data) => data.reduce(sum)/data.length
                                            exports.stddev= function(d) {
                                                let m= exports.mean(d)
                                                return Math.sqrt(d.mean(x => x - m).map(square).reduce(sum)/(d.length-1));
                                            }
                                            
                                            // 모듈 내보내기
                                            module.exports= { mean, stddev } // 공개하고자 하는 것만 내보낸다!
                                        
                                    

모듈은 단순히 일반적인 객체를 내보낼 뿐이고, 그 객체 안에 함수 프로퍼티(들)이 있을 뿐이다!


이러한 노드의 커먼 JS는 모든 버전의 노드에서 지원하는 일반적인 모듈 패턴으로서 module.exports를 사용해 함수나 클래스, 스크립트 객체를 내보낼 수 있다. 한편, 노드에서는 ES 6)의 import 문을 지원하지 않는 대신, require('모듈' 또는 'URL')로 모듈을 임포트할 수 있다: const { log, print } = require('./txt-helpers')

1. 모듈이 함수나 클래스 하나만 내보낸다면; require()를 쓰기만 하면 된다. 모듈에서 여러 프로퍼티가 있는 객체를 내보낸다면; 객체 전체를 가져올 수도 있고 해체 할당을 통해 원하는 것만 가져올 수도 있다:
                                    
                                        const stats= require('./stats.js') // stats.js 파일 전부를 가져온다

                                        const { mean, stddev } = require('./stats.js') // stats.js 파일에서 원하는 부분만 해체 할당으로 가져온다
                                    
                                
2. 사용자가 만들어 추가한 파일 모듈에 있는 내용을 모듈 바깥에서 접근할 수 있도록 module.exports를 사용하여 내보내는데, 바깥에서는 const fortune= require('./lib/fortune') 식으로 임포트해서 사용하며, fortune.getFortune() 식으로 외부 함수를 그대로 쓸 수 있다:
                                    
                                        const fortune= require('./lib/fortune') // fortune 파일 가져오기

                                        fortune.getFortune() // fortune에 내장된 getFortune 함수 호출
                                    
                                

사용할 파일에서 (원하는 이름을 붙인 인스턴스 변수로)임포트해서 사용하면 된다 - 그 인스턴스 객체 안의 함수들에는 이미 이름들이 정해져 있고, 그저 그 이름들을 써서 사용하기만 하면 된다!

Node 다루기

노드 Module은 패키지를 만들고, (스크립트의 이름 충돌을 방지하기 위하여)코드를 이름 공간 으로 구분하는 메카니즘으로서, '모듈화'와 '캡슐화'를 구현하기 위해 만든 특별한 객체이다 - 곧, 노드에서 각 파일은 비공개 네임스페이스를 가진 독립적 모듈이다!

노드의 모듈 타입
노드는 require('모듈명')이 사용되면 그 함수의 매개변수를 보고 어떤 타입인지 판단하는데, 그 찾는 순서는 [코어 모듈 > 프로젝트 루트 > node_modules 폴더 > 사용자 모듈 폴더] 순이 된다. 노드 내장 시스템 모듈이나 NPM 모듈은 파일 이름만 쓰면 되고, 사용자 정의 모듈은 현재 디렉토리에 대한 상대 경로로 작성해야 한다
[ 모듈 가져오기 및 사용하기 ]
                                        
                                            const fs= require("fs") // 노드 내장 파일시스템 모듈 가져오기
                                            const http= require("http") // 노드 내장 http 모듈 가져오기

                                            const express= require("express") // npm으로 설치한 서드파티 모듈 가져오기

                                            const req= require('req') // req 추출
    
                                            // req: 특정 사이트 긁어오기
                                            req('http://www.google.com', function(error, res, body) {
                                                console.log(body)
                                            });
                                        
                                    

  • buffer 파일, 네트워크 등 입출력 I/O 작업에 사용한다
  • stream 스트림 기반 데이터 전송에 사용한다
  • url URL 파싱 유틸리티

노드 내장 코어모듈로서 require 없이 전역으로 사용한다!

  • assert 테스트에 사용
  • child_process 외부 프로그램 실행 시 사용
  • cluster 다중 프로세서 사용
  • crypto 내장된 암호화 라이브러리
  • dns 도메인 이름 시스템(DNS) 관련
  • domain 에러를 고립시키기 위해 I/O 작업이나 기타 비동기식 작업을 그룹으로 묶음
  • events 비동기식 이벤트 지원
  • os 시스템 관련 정보
  • fs 파일시스템 관련 작업
  • path (특정 운영체제로부터 독립적인 노드만의)파일시스템 경로 탐색
  • http, https HTTP, HTTPS 서버 관련
  • net 비동기 소켓 기반 네트워크 API
  • punycode 유니코드 인코딩을 지원하며 ASCII 부분 집합을 일부 사용함
  • querystring URL 쿼리스트링을 해석하고 만드는데 사용
  • readline 대화형 I/O 유틸리티 주로, 명령줄 프로그램에서 사용된다!
  • smalloc 버퍼에 메모리를 명시적으로 할당
  • string_decoder 버퍼를 문자열로 변환
  • tls 보안 전송계층(TLS) 통신 유틸리티
  • tty 저수준 텔레타입라이터(TTY) 함수
  • dgram 사용자 데이터그램 프로토콜(UDP) 네트워크 유틸리티
  • util 내부 노드 유틸리티
  • vm 스크립트 가상 머신 메타 프로그래밍이나 컨텍스트 생성에 사용된다!
  • zlib 압축 유틸리티

npm install 모듈명으로 설치되어 node-modules 폴더에 저장되는 모듈로서, require('express') 식으로 불러올 수 있다

사용자가 추가하는 외부 모듈로서 require('./my-src/fortune') 식으로 불러쓸 수 있다

파일 시스템에 접근하기
노드에서 파일시스템에 접근하기: fs 모듈
                                    
                                        const fs= require('fs')

                                        fs.writeFile('hello.txt', '안녕?', (err) => { // 파일이 (없으면)생성하여 쓰기
                                            if (err) return console.log("Error writing to file !");
                                        }); // 생성된 hello.txt의 내용: "안녕?"
                                    
                                
                                    
                                        const fs= require('fs')

                                        fs.writeFile(__dirname + '/hello.txt', '하이!', (err) => { // 파일이 존재하면; 내용을 수정한다
                                            if (err) return console.log("Error writing to file !"); // 파일이 존재하지 않으면; 에러 메시지를 출력한다
                                        }); // 수정된 hello.txt의 내용: "하이!"
                                    
                                
1. 노드 애플리케이션을 실행하면; 해당 애플리케이션은 자신이 실행된 현재 작업 디렉토리를 __dirname 변수로 보관한다. 다만, 이 __dirname과 파일 이름을 문자열 병합으로 합치는 방식은 운영체제에 따라서는 호환되지 않을 수도 있으므로 운영체제로부터 독립적인 노드의 path 모듈이 더 확실하다:
                                    
                                        const fs= require('fs')
                                        const path= require('path') // 운영체제에 따르는 현재 경로 가져오기
                                        
                                        fs.readFile(path.join(__dirname, 'hello.txt'), { encoding: 'utf8' }, (err, data) => { // 가져온 파일 내용을 UTF8 텍스트로 인코딩한다
                                            if (err) return console.log("Error reading to file !");
                                        
                                            console.log(`Read file contents: ${data}`)
                                        }); // hello.txt 파일의 내용: "Read file contents: 하이!"
                                    
                                

readfile()은 기본값으로 바이너리 데이터를 반환하는데, writeFile()의 기본 인코딩 포맷이 utf-8 이므로 readFile()에서 읽어들일 때는 utf-8 로 인코딩해주어야 한다!

2. fs 모듈에는 파일 읽고 쓰기 외에도 readdir()(디렉토리 내 파일 검색), unlink(파일 지우기), rename(이름 바꾸기), stat(파일과 디렉토리 정보 얻기) 등이 있다:
                                    
                                        const fs= require('fs')

                                        fs.readdir(__dirname, (err, files) => { // 현재 디렉토리 내 파일 검색
                                            if (err) return console.error("Unable to read directory contents!");
                                        
                                            console.log(`${__dirname} `) // C:\_node-js\node-ex ← 현재 파일이 작업중인 경로 출력하기
                                            console.log(files.map(f => '\t' + f).join('\n')) // 각 파일명 앞에 '/t'(= 탭)을 넣고, 줄을 바꾸어(= '\n') 문자열 배열로 연결하여 출력한다
                                        });
                                    
                                
파일 스트림과 파이프
1. 스트림에는 읽기 read, 쓰기 write 등이 있는데, 이는 사용자의 타이핑, 클라이언트와 통신하는 웹 서비스 등에서 사용할 수 있다:
                                    
                                        const fs= require('fs')

                                        // 쓰기 스트림을 만들어서 쓴다
                                        const w= fs.createWriteStream('stream.txt', { encoding: 'utf8' }) // 쓰기 스트림 생성
                                        w.write('line 1\n') // 쓰기 스트림 시작
                                        w.write('line 2\n') // 쓰기 작업 계속
                                        w.end() // 쓰기 스트림 종료 ← 데이터를 단 한번만 보낸다면; w.end('line\n')로 할 수 있다!
                                    
                                
                                    
                                        const fs= require('fs')

                                        // 읽기 스트림을 만들어서 읽어온다
                                        const r= fs.createReadStream('stream.txt') // 읽기 스트림 생성
                                        const w= fs.createWriteStream('stream_copy.txt') // 쓰기 스트림 생성
                                        r.pipe(w) // 파이프를 통해 r을 w로 연결한다 ← 곧, 파일 컨테츠를 복사하는 효과로 된다! 
                                    
                                

여기서는 따로 인코딩을 필요로 하지 않는다 - 인코딩은 데이터를 해석할 때만 필요하다!

2. 읽기 와 동시에 쓰기 작업을 하는 것을 파이프라고 하는데, 이는 데이터를 옮길 때도 자주 사용된다. 예컨대, 파일 내용을 웹서버의 응답 부분에 파이프로 연결하거나, 압축 파일의 압축 해제, 파일 내용 복사 등에 이용된다:
                                    
                                        const fs= require('fs')

                                        const r= fs.createReadStream('stream.txt', { encoding: 'utf8' }) // 읽기 스트림 생성
                                        r.on('data', function(data) {
                                            console.log('>> data: ' + data.replace('\n', '\\n')) // 줄바꿈 문자의 이스케이프 처리
                                        });
                                        
                                        r.on('end', function(data) {
                                            console.log('>> end')
                                        });
                                    
                                
3. ServerRequest 객체는 쓰기 스트림 인터페이스이며 이를 통해 데이터를 클라이언트로 보내는데, 쓰기 스트림이기에 파일을 보내기도 쉽다. 예컨대, 파일 읽기 스트림을 만들어 HTTP 응답에 파이프로 연결하기만 하면 된다:
                                    
                                        const http= require('http')
                                        const port= 3000
                                        
                                        const server= http.createServer((req, res) => { /* 파비콘 요청에 대한 처리 */
                                            if (req.method === 'GET' && req.url === '/favicon.ico') { // 웹사이트가 파비콘을 요청한다면;
                                                const fs= require('fs')
                                                fs.createReadStream('favicon.ico')
                                                fs.pipe(res) // end() 대신 사용할 수 있다!
                                            } else {
                                                console.log(`${req.method} ${req.url}`)
                                                res.end('Hello, world!')
                                            }
                                        })
                                        
                                        server.listen(port, function() { // 서버 시작 시 호출될 콜백함수
                                            console.log(`Server started on port ${port}`)
                                        });
                                    
                                

http 요청은 GETURL 경로로 구성되는데, 대부분의 브라우저는 요청을 보낼 때 탭에 표시할 아이콘인 파비콘 도 함께 요청한다!

노드의 오류우선 콜백

노드의 오류우선 콜백
노드는 일반적으로 인자 두개로 호출되는 오류우선 콜백을 사용한다. 첫번째 인자는 일반적으로 null 이며(= 오류 없음 - 아니라면; 뭔가 문제가 있는 상태이다!), 두번째 인자는 원래 비동기 함수의 데이터 또는 응답이다:
                                    
                                        const fs= require("fs")

                                        function readConfigFile(path, callback) {
                                            fs.readFile(path, "utf8", (err, text) => { // 파일(path)을 읽어와 utf8로 분석하고, 그 결과값을 콜백으로 전달한다
                                                if (err) { // 파일 읽기에 실패하면;
                                                    console.error(err)
                                                    callback(null)
                                                    return;
                                                }
                                        
                                                let data= null
                                                try {
                                                    data= JSON.parse(text)
                                                } catch(e) { // 파일(text) 분석중 에러가 생기면;
                                                    console.error(e)
                                                }

                                                callback(data)
                                            });
                                        }
                                    
                                
1. 위 코드는 프라미스 코드로 반환해주는 util.promisify() 래퍼 함수를 써서 프라미스 코드로 변형해줄 수 있다:
                                    
                                        const util= require("util")
                                        const fs= require("fs")
                                        const pfs= { readFile: util.promisify(fs.readFile) }
                                        
                                        function readConfigFile(path) { // 프라미스로 반환하는 함수
                                            return pfs.readFile(path, "utf-8").then(text => {
                                                return JSON.parse(text);
                                            });
                                        }
                                    
                                

util.promisify()는 노드의 콜백 기반 함수를 프라미스 버전으로 변형해주는데, 이와 같이 노드의 파일시스템 관련 함수를 프라미스 기반으로 미리 바꾸어둔 함수들은 fs.promises 객체에 다수 존재한다 참고로, 위 코드의 pfs.readFile()fs.promises.readFile()로 대체할 수 있다!

2. 위 코드는 또한 다음과 같이 async .. await 문으로 재작성해줄 수 있다:
                                    
                                        const fs= require("fs")
                                        const util= require("util")
                                        const pfs= { readFile: util.promisify(fs.readFile) }
                                        
                                        async function readConfigFile(path) {
                                            let text= await pfs.readFile(path, "utf-8");

                                            return JSON.parse(text);
                                        }
                                    
                                

Node 웹서버

노드에서는 앱이 곧 웹서버 이며, 노드는 단지 웹서버를 만들 수 있는 프레임워크를 제공하는 플랫폼일 뿐이다!

노드 기본 웹서버
노드는 클라이언트 유저가 한 행위에 대해 서버에서 처리하는 이벤트 주도 프로그래밍으로서, 유저의 이벤트에 어떻게 반응할 지를 정하는 것이 바로 라우팅이다 - 라우팅은 클라이언트가 요청한 컨텐츠를 전송하는 메카니즘으로서, URL 로 전달된다
[ 기본 노드 웹서버 ]
                                        
                                            const http= require('http') // http 요청
                                            const port= process.env.PORT || 3000 // 포트 설정 http://localhost:3000/

                                            const server= http.createServer((req, res) => { // http 요청에 대한 응답
                                                res.writeHead(200, { 'Content-Type': 'text/plain' })
                                                res.end('Kjc Homepage') // 웹브라우저에 표시할 내용
                                            })

                                            // 서버 활성화: 포트 번호와 서버 시작 시 메시지 전달
                                            server.listen(port, () => console.log(`Server started on port ${port} ` + "press Ctrl-C to terminate.."));
                                        
                                    

Windows PowerShell에서 node server.js 명령으로 server.js 웹서버를 실행하고, 웹브라우저 주소표시줄에서 http://localhost:3000/으로 서버에 접속한다 localhost(= 127.0.0.1)는 현재 사용중인 컴퓨터를 뜻한다!

1. http 요청GET(또는, POST 등)의 메서드와 URL 경로(경로와 쿼리스트링, 헤더, 도메인, IP주소 등)로 구성되는데, 대부분의 브라우저는 요청을 보낼 때 GET (또는, POST ) 외에 파비콘 도 함께 요청한다. 이 요청에 응답하는 http.creatServer((req, res) => { .. })는 매개변수로 모든 http 요청 정보가 들어있는 요청 객체(req )와 클라이언트에 보낼 응답을 컨트롤하는 프로퍼티와 메서드가 들어있는 응답 객체(res )를 받게 된다:
                                    
                                        /* server-2.js */
                                        const http= require('http')
                                        const port= process.env.PORT || 3000 // 포트 설정 http://localhost:3000/

                                        const server= http.createServer(function(req, res) { // 웹서버 설정 및 요청을 처리할 콜백 함수
                                            console.log(`${req.method} ${req.url}`) // 요청은 GET method와 url로 구성된다
                                            res.end('hello, world!') // 서버측 응답
                                        });

                                        server.listen(port, function() { // 서버 시작 ← 서버 시작 시 호출될 콜백을 넘길 수도 있다!
                                            console.log(`Server started on port ${port}, ` + "press Ctrl-C to terminate..")
                                        });                                    
                                    
                                
2. 라우팅은 클라이언트가 요청한 컨텐츠를 전송하는 메카니즘으로서, 웹 기반 클라이언트/서버 애플리케이션에서 클라이언트는 원하는 컨텐츠를 URL (경로와 쿼리스트링)으로 요청한다 서버 라우팅은 경로와 쿼리스트링에 의존하지만, 요청에는 헤더, 도메인, IP주소 같은 다른 정보들도 들어있다!
                                    
                                    /* server-3.js */
                                    const http= require('http') // http 요청
                                    const port= process.env.PORT || 3000 // 포트 설정 http://localhost:3000/

                                    const server= http.createServer((req, res) => { // http 요청에 응답하는 서버 생성
                                        // 쿼리스트링 옵션인 마지막 슬래시를 없애고 소문자로 바꿔서 URL을 정규식화한다
                                        const path= req.url.replace(/\/?(?:\?.*)?$/, '').toLowerCase(); // http://localhost:3000/about?test=1#history=express

                                        switch (path) {
                                            case '': // 프로젝트 루트로 이동
                                                res.writeHead(200, {'Content-Type': 'text/plain'})
                                                res.end('Homepage')
                                                break
                                            case '/about': // '/about' 페이지로 이동
                                                res.writeHead(200, {'Content-Type': 'text/plain'})
                                                res.end('About Page')
                                                break
                                            default: // 해당하는 페이지 없음
                                                res.writeHead(404, {'Content-Type': 'text/plain'})
                                                res.end('Not Found!')
                                                break
                                        }
                                    });

                                    server.listen(port, () => console.log(`Server started on port ${port}, ` + "press Ctrl-C to terminate.."));                                    
                                

이제 서버를 실행하면; http://localhost:3000이나 http://localhost:3000/about 페이지로 이동할 수 있다 없는 페이지인 http://localhost:3000/src로도 이동해보십시오..

3. 바뀌지 않는 Static 정적 자원들은 public 폴더에 넣어준다. 이제 서버를 실행하고, http://localhost:3000/about으로 이동하면; public/about.html 파일이 전송된다:
                                    
                                        /* server-4.js */
                                        const http= require('http') // http 요청
                                        const fs= require('fs')
                                        const port= process.env.PORT || 3000 // 포트 설정 http://localhost:3000/

                                        function serverStaticFile(res, path, contentType, resCode=200) { // 정적 자원 전송
                                            fs.readFile(__dirname + path, (err, data) => { // 콜백 함수
                                                if (err) { // 파일이 존재하지 않거나 권한 문제로 읽어들일 수 없다면;
                                                    res.writeHead(500, {'Content-Type': 'text/plain'})
                                                    return res.end("500 - Internal Error!"); // 서버 에러 500
                                                }

                                                res.writeHead(resCode, {'Content-Type': contentType}) // 성공 코드 200
                                                res.end(data) // 읽어들인 컨텐츠를 클라이언트로 전송한다
                                            });
                                        }

                                        const server= http.createServer((req, res) => { // http 요청에 응답하는 서버 생성
                                            // 쿼리스트링 옵션인 마지막 슬래시를 없애고 소문자로 바꿔서 URL을 정규식화한다
                                            const path= req.url.replace(/\/?(?:\?.*)?$/, '').toLowerCase(); // http://localhost:3000/about?test=1#history=express

                                            switch(path){
                                                case '': // 홈페이지로 이동
                                                    serverStaticFile(res, '/public/home.html', 'text/html')
                                                    break
                                                case '/about': // 어바웃 페이지로 이동
                                                    serverStaticFile(res, '/public/about.html', 'text/html')
                                                    break
                                                case '/img/favicon.ico':
                                                    serverStaticFile(res, '/public/img/favicon.ico', 'image/ico')
                                                    break
                                                default: // 해당하는 페이지 없음
                                                    serverStaticFile(res, '/public/404.html', 'text/html', 404)
                                                    break
                                            }
                                        })

                                        // 서버 활성화: 포트 번호와 서버 시작 시 메시지 전달
                                        server.listen(port, () => console.log(`Server started on port ${port} ` + "press Ctrl-C to terminate.."));
                                    
                                

fs.readFile()은 파일을 비동기식으로 읽어들이는 콜백 함수로서 지정한 파일의 컨텐츠를 읽고, 다 읽은 다음에는 콜백 함수를 실행하여 반환한다 __dirname은 현재 실행중인 스크립트가 존재하는 작업 디렉토리를 말한다!

Express Start..

먼저, 프로젝트 서버용 루트 폴더를 만들고, 쉘 커맨드에서 npm init -y를 실행하고, npm install express로 익스프레스를 설치해준다

익스프레스 기본 웹서버
익스프레스 웹서버 파일 index.js 를 만들어준 폴더의 쉘 커맨드에서 node index.js로 서버를 실행하고, 브라우저의 주소표시줄에서 http://localhost:3000/으로 웹서버에 접속할 수 있다
[ 익스프레스 기본 웹서버 ]
                                        
                                            /* 익스프레스 웹서버 파일: index.js ← 프로젝트 진입점이 된다! */
                                            const express= require('express') // express 모듈 추출
                                            const app= express() // 익스프레스 웹서버 생성

                                            app.use(function(req, res) { // 익스프레스 요청과 응답
                                                res.send("<h1>hello, world!</h1>")
                                            });

                                            app.listen(3000, function() { // 포트 3000에서 익스프레스 웹서버 실행
                                                console.log('Express Server Running: http://localhost:3000') // 웹서버 실행 메시지
                                            });
                                        
                                    

웹서버 파일명을 바꾸게 되면; package.json 파일을 열어 수정해주어야 한다. 한편, 웹서버 파일의 내용을 변경한 경우에는; ([Windows PowerShell]이라면;)쉘 커맨드에서 [Ctrl-C](서버 종료)와 (직전 명령)로 웹서버를 다시 실행해준다!

                                                
                                                    {
                                                        "name": "ex1", // 프로젝트 루트 폴더명
                                                        "version": "1.0.0",
                                                        "main": "index.js", // 웹서버 파일 ← 'npm init -y'의 기본값인 index.js가 아니라면; 여기서 바꿔주면 된다!
                                                        "scripts": {
                                                            "test": "echo \"Error: no test specified\" && exit 1"
                                                        },
                                                        "keywords": [],
                                                        "author": "Kjc",
                                                        "license": "ISC",
                                                        "description": "", // 프로젝트 기본 설정 == Readme.md 임시 설명
                                                        "dependencies": {
                                                            "express": "^5.1.0"
                                                        }
                                                    }
                                                
                                            
정적 미들웨어
1. 이미지, css 파일 및 Javascript 파일과 같은 정적 파일을 제공할 때는 express.static(root[, options]) 미들웨어 기능을 사용한다
[ 미들웨어의 작동방식 ]
                                        
                                            
                                        
                                    
2. 아래 코드에서, 정적 Static 미들웨어는 기본적으로 접속한 ip의 루트에 있는 public 폴더 안 index.html 파일을 실행하는데, index.html 파일이 없거나 라우트하지 않은 딴 경로로 들어가게 되면; 다음에 나오는 미들웨어를 수행하게 된다:
[ Static 미들웨어 ]
                                        
                                            
                                        
                                    

public 폴더에 index.html 파일이 있다면; 아래 미들웨어는 실행되지 않는다. 한편, 위 미들웨어의 순서를 바꾸면; 정적 미들웨어는 실행되지 않는다 아래 미들웨어로 내려가도록 하는 next() 호출이 없다!


이제, public 폴더에 있는 정적 파일(예컨대, home.html)을 써서 http://localhost:3000/home.html으로 접속하거나, http://localhost:3000/img/favicon.ico와 같이 이미지를 불러올 수도 있다. 이 public 폴더 안에 있는 것들은 무조건 클라이언트로 전송되며, 컨텐츠 타입도 자동으로 설정된다!


✓   Express는 정적 폴더를 기준으로 파일을 조회하므로 정적 폴더의 이름은 URL의 일부가 아니다. 또한, express.static()에 제공하는 경로는 노드 프로세스를 시작한 폴더가 기준이 되므로, 절대경로를 사용하는 것이 안전하다!

서버 라우팅

HTTP 통신에서는 클라이언트가 서버에 요청 메시지를 보내고, 이에 서버는 클라이언트에 응답 메시지를 보내는데, 이는 TCP/IP 전송 프로토콜에 의거하여 IP주소:포트를 통해 연결된다!

서버 라우팅
1. Routing은 사용자의 요청에 따라 사용자가 필요로 하는 정보를 제공하는 미들웨어이다. 곧, 라우팅은 앱이 URI(대개, URL)이나 특정 엔드포인트에 대한 HTTP 요청 메서드(GET, POST 등)에 응답하는 방법을 결정하는 것으로서, 따로 설정하지 않아도 자동으로 사용된다
                                    
                                        
                                    
                                

이제 http://localhost:3000/, http://localhost:3000/src 등으로 각각의 경로에 접근할 수 있다!

2. 익스프레스에서는 라우트미들웨어의 순서가 중요하다. 미들웨어는 라우트가 일치하지 않을 때 수행되며, 따라서 미들웨어는 라우트보다 밑에 두어야 한다:
                                    
                                        /* index.js */
                                        const express= require('express')
                                        const app= express()
                                        const port= process.env.PORT || 3000; // 동적 포트 설정 ← 명시적으로 포트 번호를 지정하지 않은 경우; 3000번을 사용한다!

                                        app.use(express.static('public')) // public: Static 미들웨어에 제공할 파일이 들어있는 폴더

                                        // 루트 라우트 설정: app.get('라우팅 경로', 콜백 함수(req, res) => { .. })
                                        app.get('/', (req, res) => { // 루트 페이지 ← 콜백 함수는 라우트가 일치할 때 호출되는데, 요청과 응답 객체가 매개변수로 전달된다
                                            res.type('text/plain') // Content-Type 헤더 설정
                                            res.send('Kjc의 Web Server 홈') // 노드의 res.end 대신 res.send 사용!
                                        });

                                        // /src 페이지 라우트
                                        app.get('/src', (req, res) => {
                                            res.type('text/plain')
                                            res.send('Kjc의 /src 페이지')
                                        }); // 익스프레스에서는 기본적으로 상태코드 200을 반환하므로 직접 작성할 필요는 없다!

                                        // app.use 미들웨어는 라우트가 일치하지 않을 때 수행된다 - 따라서, 미들웨어는 라우트보다 밑에 두어야 한다!
                                        app.use((req, res) => { // 커스텀 404 페이지
                                            res.type('text/plain')
                                            res.status(404).send('404 - Not Found !')
                                        });

                                        app.use((err, req, res, next) => { // 커스텀 500 에러 페이지
                                            console.error(err.message)
                                            res.type('text/plain')
                                            res.status(500).send('500 - Server Error !')
                                        });

                                        // 서버 활성화: 포트 번호와 서버 시작 시 메시지 전달
                                        app.listen(port, () => console.log(
                                            `Express started on http://localhost:${port} ` + `Press Ctrl-C to terminate.`
                                        ));
                                    
                                

✓   익스프레스에서 라우트 경로는 기본적으로 대/소문자를 구분하지 않고, 끝에 /가 있든 없든 상관하지 않으며, 쿼리스트링(?foo=bar) 또한 무시한다 따라서, 노드 웹서버에서와는 달리 http 요청을 받아 req.url을 써서 어렵게 정규식화하는 작업은 더이상 필요로 하지 않는다!

➥ HTTP 상태코드 값

res.status(code)에서 익스프레스의 기본값이 200(= OK)이므로 404(= 없음), 500(= 서버 오류) 등의 상태코드를 반환할 경우에만 사용하면 된다 - 브라우저 리디렉트(상태코드 301, 302, 303, 307)는 res.redirect([status, ]url)(기본값: 302- 발견됨) 메서드를 사용하는 것이 좋은데, 일반적으로 페이지를 영구히 이동한 경우(상태코드 301) 외에는 자주 쓰지 않는 것이 좋다 참고로, res.status()는 응답 객체를 반환하므로 메서드 체인으로 연결할 수 있다: res.status(404).send('Not Founded!')

3. 서버측 응답 메서드에는 app.get/post/put/del()이 있고, 모든 HTTP 요청 메서드의 경로에서 미들웨어를 로드하는 데 사용되는 특별한 라우팅 메서드인 app.all()도 있다:
                                    
                                        app.all('/secret', (req, res, next) => {
                                            console.log('Accessing the secret section ..')
                                            next()
                                        });
                                    
                                

이 처리기는 GET, POST, PUT, DELETE 또는 http 모듈에서 지원되는 다른 HTTP 요청 메서드를 사용하는지 여부와 무관하게 "/secret" 경로에 대한 요청을 위해 수행된다!

클라이언트측 요청 메서드와 요청 헤더
브라우저는 웹사이트에 방문할 때마다 서버에 '보이지 않는' 정보들(돌려받을 페이지에 관련한 요청 사항, 사용자 에이전트에 관한 정보 등..)을 보내는데, 이 정보들은 모두 요청 헤더로 전달되며 요청 객체의 req.headers 프로퍼티(노드 기본 제공)를 통해 그 이름 을 확인할 수 있다:
                                    
                                        /* 요청 헤더로 전달되는 정보 확인하기 */
                                        const express= require('express')
                                        const app= express()
                                        
                                        // 요청 헤더로 전달되는 정보 확인
                                        app.get('/headers', (req, res) => {
                                          const headers= Object.entries(req.headers).map(([key, value]) => `${key}: ${value}`)
                                          res.type('text/plain')
                                          res.send(headers.join('\n'))
                                        })
                                        
                                        const port= process.env.PORT || 3000
                                        app.listen(port, () => console.log(`\nnavigate to http://localhost:${port}/headers\n`))
                                    
                                

참고로, http://localhost:3000/headers로 접속하여 [관리자모드]로 들어가서 네트워크 탭을 클릭하고, 페이지를 '새로 고침'한 뒤 Doc(또는, HTML) 탭의 아래쪽 headers를 클릭해보면 요청 내용과 요청/응답 헤더 내용을 상세히 확인할 수 있다!

요청 객체(첫번째 매개변수)는 노드 객체 http.IncomingMessage의 인스턴스로 시작하는데, 보통 reqrequest라는 이름으로 사용한다:

  • req.protocal 요청에 사용된 프로토콜: http://
  • req.ip 클라이언트의 ip 주소: www.sosohan.xyz, localhost
  • req.path 요청 경로: /about
  • req.url/originalUrl 요청 경로와 쿼리스트링(노드 기본 제공): /about?test=1 req.url은 내부 라우팅 목적으로 고쳐쓸 수 있지만, req.originalUrl은 원래 요청 그대로 보존하도록 설계되었다!
  • req.route 현재 일치하는 라우트에 관한 정보 주로 라우트 디버깅에 사용된다!
  • req.hostname 클라이언트에서 전송한 호스트 이름인가? 이 정보는 위조될 수 있으므로 보안 목적으로 사용해서는 안된다!
  • req.cookies/signedCookies 클라이언트에서 전송한 쿠키값
  • req.params 이름붙은 라우트 매개변수를 담은 배열
  • req.query 쿼리스트링 매개변수(Get 매개변수)
  • req.body Post 매개변수 Post 매개변수는 요청 body에 들어 있으며, 컨텐츠 타입을 분석할 수 있는 미들웨어가 필요하다!
  • req.accepts(types) 클라이언트가 주어진 types 를 받아들이는가? types에는 MIME 타입, 콤마로 구분한 리스트, 배열 등이 들어갈 수 있다!
  • req.xhr Ajax 호출에 의한 요청인가?
  • req.sequre 암호화된 연결인가? req.protocol === 'https'와 같다!
서버측 응답 메서드와 응답 헤더
브라우저가 요청 헤더 형태로 서버에 정보를 보내듯이, 서버 또한 많은 정보들(메타 데이터와 서버 정보)을 브라우저로 보내는데, 거기에는 브라우저가 보낸 Content-Type 헤더에 따르는 렌더링 정보, 응답의 압축 여부, 사용하는 인코딩 방식, 캐시 유효기간 정보 등이 포함된다

✓   서버가 보내오는 정보에는 서버에 관한 정보가 포함될 수도 있는데, 이 경우 해커의 표적이 될 위험성도 있다. 이에, 다음과 같이 설정해주면; 서버 정보를 비활성화할 수 있다: app.disable('x-powered-by')

➥ 인터넷 미디어타입

브라우저는 컨텐츠 렌더링 시, 기본적으로 Content-Type 정보(text/plain, text/html, text/css, text/xml, image/jpeg, image/png, video/mpeg, audio/mp3, ..)를 사용해 렌더링 방법을 결정하는데, Content-Type 헤더 형식은 타입/서브타입 및 (옵션인)매개변수로 구성된다: text/html; charset=UTF-8

res.send(body)는 클라이언트에 응답을 보낸다 - 익스프레스의 기본 컨텐츠 타입은 text/html이므로 text/plain으로 바꾸려는 경우; 먼저 res.type('text/plain')을 호출해야 한다. body가 객체나 배열인 경우 응답은 JSON으로 전송되며 컨텐츠 타입 또한 그에 맞게 자동으로 바뀌지만, JSON을 전송할 때는 res.json(json)을 사용하는 것이 알기 쉽다. 한편, res.end는 응답 없이 연결을 끊는다:
                                    
                                        
                                    
                                

이제 http://localhost:3000/data.html, http://localhost:3000/data.json, http://localhost:3000/data.xml로 접속하여 파일 내용을 확인해보십시오..


res.send(body)body 가 문자열이면 html 형식으로, 배열이나 객체에는 JSON 형식으로 응답한다. XML 형식으로 응답하도록 하려면; res.type('text/xml')로 응답 형식을 지정해주어야 한다 참고로, res.send()에서 data/json으로 응답할 때; 내부적으로는 다음 코드와 같이 구성된다: res.type('application/json'); res.send(JSON.stringify(output))

응답 객체(두번째 매개변수)는 노드 객체 http.ServerResponse의 인스턴스로 시작하는데, 보통 reqrequest 라는 이름으로 사용한다:

  • res.locals 뷰 렌더링의 기본 컨텍스트를 담은 객체
  • res.status(코드) HTTP 상태코드 익스프레스 기본값이 200(OK)이므로, 그 이외의 경우에만 사용하면 된다!
  • res.redirect(코드) 브라우저를 리디렉트한다 익스프레스 기본값은 302(발견됨)인데, 301(페이지를 완전히 벗어남) 외에는 쓰지 않는 것이 좋다!
  • res.cookie(name, value[, options]) 클라이언트에 저장될 쿠키 설정 미들웨어가 필요하다!
  • res.clearCookie(name[, options]) 클라이언트에 저장된 쿠키 삭제 미들웨어가 필요하다!
  • res.json/jsonp(json) Json/Jsonp 전송
  • res.download(path[, filename, callback]) Content-Disposition 응답 헤더를 attatchment 로 설정하여 브라우저가 컨텐츠를 렌더링하지 않고 파일 형태로 내려받을 수 있도록 한다
  • res.sendFile(path[, filename, callback]) path 로 지정한 파일을 읽고 그 컨텐츠를 클라이언트로 전송하는데, 실제 사용할 이유는 별로 없다 일반적으로는 클라이언트에 보낼 파일을 public 디렉토리에 보관하고 static 미들웨어를 사용하는 것이 더 편하다 - 단, 같은 URL에서 조건에 따라 다른 자원을 전송하고자 할 때는 유용하게 사용할 수 있다!
  • res.links Links 응답 헤더 설정
  • res.accepts(types) 클라이언트가 주어진 타입을 받아들이는지 여부 확인 types에는 인터넷 미디어타입, 콤마로 구분한 리스트, 배열을 사용할 수 있다

컨텐츠 렌더링

컨텐츠 렌더링 기본
컨텐츠를 렌더링할 때는 뷰를 레이아웃 안에 렌더링하는 res..render가 주로 사용되는데, req.query로 쿼리스트링 값을 가져오거나, req.session으로 세션값을 가져오거나, req.cookies로 쿠키값을 가져올 수도 있다:
                                    
                                        /* ex.js - 라우팅 기본 사용법 */
                                        const express= require('express')
                                        const app= express()

                                        // 웹서버 포트 설정:
                                        const port= process.env.PORT || 3000;

                                        // 핸들바 뷰엔진 설정
                                        const expressHandlebars= require('express-handlebars').create({ defaultLayout: 'main' })
                                        app.engine('handlebars', expressHandlebars.engine)
                                        app.set('view engine', 'handlebars') // 이제 main 템플릿이 모든 뷰의 레이아웃으로 사용된다!

                                        // 루트 라우팅:
                                        app.get('/', (req, res) => {
                                            res.render('home') // GET 엔드포인트에서 res.render()는 기본 값으로 응답 코드 200을 반환한다!
                                        })

                                        // /about 라우팅:
                                        app.get('/about', (req, res) => {
                                            res.render('about')
                                        })

                                        // 미들웨어: 커스텀 500 에러 페이지:
                                        app.use((error, req, res, next) => { // 다음으로 넘어가기 위해서는 (사용하지 않더라도) next가 필요하다!
                                            res.status(500) // 200 이외의 다른 응답 코드가 필요하다면; res.status(응답 코드)를 사용해야 한다!
                                            res.render('500 - Server Error !')
                                        })

                                        // 미들웨어: 커스텀 404 페이지 ← 커스텀 에러는 맨 끝에 위치해야 한다!
                                        app.use((req, res) => {
                                            res.status(404).render('404 - Not Found !') // 메서드 체이닝으로 연결함
                                        });

                                        // 웹서버 활성화: 서버 시작 시 메시지 전달
                                        app.listen(port, () => console.log(
                                            `Express started on http://localhost:${port} ` + `Press Ctrl-C to terminate..`
                                        ));
                                    
                                

res.renderlocals 매개변수는 res.locals의 컨텍스트를 덮어쓰지만, 덮어쓰여지지 않은 컨텍스트는 여전히 사용할 수 있다는 점에서 차이가 있다!

1. res.render()는 기본값으로 응답코드 200 을 반환하므로, 다른 응답 코드가 필요한 경우에는 res.status(코드)도 함께 사용해야 한다!
                                    
                                        /* 200 응답 코드 */
                                        app.get('/', (req, res) => {
                                            res.render('home') // GET 엔드포인트에서 res.render()는 기본 값으로 응답 코드 200을 반환한다!
                                        });

                                        app.get('/about', (req, res) => {
                                            res.render('about')
                                        });

                                        // 200 이외의 에러 핸들러 추가
                                        app.get('/error', (req, res) => res.status(500).render('error'));
                                    
                                
2. 평문 렌더링
                                    
                                        app.get('/', (req, res) => { res.render('home') });
                                        app.get('/about', (req, res) => { res.render('about') });
                                        
                                        // 평문 렌더링:
                                        app.get('/text', (req, res) => { res.type('text/plain').send('this is a test') });
                                                                                
                                        app.get('*', (req, res) => res.send('Check out our "<a href="/about">About</a>" page!'));
                                    
                                
3. 뷰에 쿼리스트링, 쿠키, 세션 값 등의 컨텍스트 전달
                                    
                                        /* 뷰에 쿼리스트링, 쿠키, 세션 값 등의 컨텍스트 전달 */
                                        const fortune= require('./lib/study') // 사용자 모듈 stydy 임포트

                                        const express= require('express')
                                        const app= express()

                                        const cookieParser= require('cookie-parser') // 이하, 모두 npm install 모듈명으로 설치해주어야 한다!
                                        const session= require('express-session')

                                        const catNames= require('cat-names')

                                        const expressHandlebars= require('express-handlebars').create({ defaultLayout: 'main' })
                                        app.engine('handlebars', expressHandlebars.engine)
                                        app.set('view engine', 'handlebars')

                                        // Static 미들웨어:
                                        app.use(express.static(__dirname + '/public'))

                                        app.use(cookieParser()) // for cookie support
                                        app.use(session({resave: false, saveUninitialized: false, secret: 'keyboard cat'})) // for session support

                                        // see the views/greeting.hbs file for the contents of this view
                                        app.get('/greeting', (req, res) => {
                                            res.render('greeting', {
                                                message: 'Hello esteemed programmer!',
                                                style: req.query.style,
                                                userid: req.cookies.userid,
                                                username: req.session.username
                                            });
                                        });

                                        app.get('/set-random-userid', (req, res) => {
                                            res.cookie('userid', (Math.random()*10000).toFixed(0))
                                            res.redirect('/greeting')
                                        });

                                        app.get('/set-random-username', (req, res) => {
                                            req.session.username= catNames.random()
                                            res.redirect('/greeting')
                                        });

                                        app.get('*', (req, res) => res.send('Check out our greeting page!'));

                                        // 서버 활성화: 포트 번호와 서버 시작 시 메시지 전달
                                        const port= process.env.PORT || 7000;
                                        app.listen(port, () => console.log(`Express started on http://localhost:${port} ` + `Press Ctrl-C to terminate..`));
                                    
                                

핸들바 뷰엔진

는 사용자가 보는 것을 담당하는 부분으로서, html만 아니라 PDF 등 클라이언트가 렌더링할 수 있는 것은 모두 뷰라고 볼 수 있는데, 뷰는 즉석에서 동적으로 변할 수 있다는 점에서 이미지나 Css 파일 같은 정적 자원과는 다르다!

핸들바 뷰 엔진
1. 템플릿을 렌더링할 때는 템플릿 엔진에 컨텍스트를 전달하며, 이를 통해 템플릿에 데이터가 삽입된다. 먼저, 커맨드 쉘에서 npm install express-handlebars로 핸들바 뷰 엔진을 설치하고, 핸들바 뷰 엔진을 설정해준다:
                                    
                                        /* cite.js - 핸들바 템플릿 사용 */
                                        const express= require('express')
                                        const app= express()

                                        // 핸들바 뷰엔진 설정 ← express-handlebars로 최신 버전을 설치하는 경우
                                        const expressHandlebars= require('express-handlebars').create({ defaultLayout: 'main' });
                                        app.engine('handlebars', expressHandlebars.engine)
                                        app.set('view engine', 'handlebars')

                                        // 웹서버 포트 설정:
                                        const port= process.env.PORT || 7000;

                                        // 루트 라우팅:
                                        app.get('/', (req, res) => { res.render('home') });

                                        // /about 라우팅:
                                        app.get('/about', (req, res) => { res.render('about') });

                                        // 커스텀 404 페이지:
                                        app.use((req, res) => {
                                            res.status(404).render('404 - Not Found !');
                                        })

                                        // 커스텀 500 에러 페이지:
                                        app.use((err, req, res, next) => {
                                            console.error(err.message)
                                            res.status(500).render('500 - Server Error !');
                                        })

                                        // 서버 활성화: 포트 번호와 서버 시작 시 메시지 전달
                                        app.listen(port, () => console.log(
                                            `Express started on http://localhost:${port}, Press Ctrl-C to terminate`
                                        ));
                                    
                                

라우트(루트와 어바웃 페이지)에서는 뷰 엔진에서 컨텐츠 타입(text/html)과 상태 코드(200)을 기본으로 반환하므로 따로 명시할 필요가 없다 커스텀 페이지에서는 상태 코드를 명확히 적어주어야 한다!

2. 웹서버에 핸들바 뷰 엔진을 설정한 다음, 루트 폴더에서 views 폴더와 그 하위 폴더 layouts를 만들고 main.handlebars 파일을 작성해준다 이 템플릿 파일이 모든 뷰의 기본 레이아웃으로 사용된다!
                                    
                                        
                                    
                                

main 뷰의 {{{body}}} 부분은 각 뷰에서 html로 바뀐다!


✓   왜 중괄호가 3개씩이나 있나요? 템플릿을 렌더링할 때는 템플릿 엔진에 컨텍스트 객체를 전달하며, 이렇게 데이터를 삽입합니다:

                                    
                                        /* 컨텍스트 객체 */
                                        { name: 'Fruits' }
                                    
                                
                                    
                                        
                                    
                                
                                    
                                        /* 컨텍스트 객체 */
                                        { name: '<i>Fruits</i>' }
                                    
                                

내부에 html 코드가 있는 경우에는, 중괄호 3개를 쓰면; html 코드의 이스케이프를 막고, html 코드를 제대로 실행해줍니다: hello, Fruits !

3. 계속해서, views 폴더에 아래 각각의 내용으로 핸들바 파일들을 만들어준다: home.handlebars, about.handlebars, 404.handlebars, 500.handlebars
                                    
                                        
                                    
                                
뷰의 동적 컨텐츠
는 단순히 정적 자원만이 아니라 동적 컨텐츠 또한 포함할 수 있다 - 먼저, about.handlebars 를 다음과 같이 수정해주고, 웹서버 cite.js 에서도 fortunes 데이터를 추가해준 뒤, about 라우팅 부분도 수정해준다:
                                    
                                        
                                    
                                
                                    
                                        const express= require('express')
                                        const app= express()

                                        const fortunes= [ // 캡슐화: 이 부분은 바깥에서 볼 수 없다!
                                            "html 5", "Css 3", "Bootstrap 5", "JavaScript",
                                        ]

                                        // 핸들바 뷰엔진 설정 ← express-handlebars로 최신 버전을 설치하는 경우
                                        const expressHandlebars= require('express-handlebars').create({ defaultLayout: 'main' });
                                        app.engine('handlebars', expressHandlebars.engine)
                                        app.set('view engine', 'handlebars')

                                        // 웹서버 포트 설정:
                                        const port= process.env.PORT || 7000;

                                        ..
                                    
                                
정적 파일과 뷰: Static 미들웨어
익스프레스는 기능별로 모듈화한 미들웨어를 사용해 정적 파일과 뷰를 처리한다: 정적 미들웨어public 폴더에 정적 자원(이미지, Css 파일, 클라이언트 사이드 스크립트 파일 등..)을 보관하고, 이들은 아무런 변경없이 바로 클라이언트로 전송된다
프로젝트 루트에 public 폴더를 만들고, 그 아래 img 폴더를 만들어 로고 이미지와 파비콘 이미지를 넣어준 뒤 메인 핸들바를 다음과 같이 수정해 준다:
                                    
                                        
                                    
                                
이어서, 웹서버 파일의 라우팅 선언 바로 앞 부분에 다음 코드를 추가해준다: app.use(express.static(__dirname + '/public' )) __dirname은 현재 앱이 실행된 디렉토리이다!
                                    
                                        const express= require('express')
                                        const app= express()
                                        
                                        // 핸들바 뷰엔진 설정
                                        const expressHandlebars= require('express-handlebars').create({ defaultLayout: 'main' })
                                        app.engine('handlebars', expressHandlebars.engine)
                                        app.set('view engine', 'handlebars')
                                        
                                        // 웹서버 포트 설정:
                                        const port= process.env.PORT || 7000
                                        
                                        // Static 미들웨어:
                                        app.use(express.static(__dirname + '/public' ))
                                        
                                        // 루트 라우팅:
                                        app.get('/', (req, res) => { res.render('home') })
                                        
                                        // /about 라우팅:
                                        app.get('/about', (req, res) => { res.render('about') })
                                        
                                        // 커스텀 404 페이지:
                                        app.use((req, res) => {
                                            res.status(404)
                                            res.render('404 - Not Found !')
                                        })
                                        
                                        // 커스텀 500 에러 페이지:
                                        app.use((err, req, res, next) => {
                                            console.error(err.message)
                                            res.status(500)
                                            res.render('500 - Server Error !')
                                        })
                                        
                                        // 서버 활성화: 포트 번호와 서버 시작 시 메시지 전달
                                        app.listen(port, () => console.log(
                                            `Express started on http://localhost:${port} ` + `Press Ctrl-C to terminate..`
                                        ))
                                    
                                
* 참고 Static 미들웨어는 전송하려는 정적 파일 각각에 파일을 렌더링하고 클라이언트에 반환하는 라우트를 지정한 것과 같은 효과를 낸다: public/img 폴더에 있는 이미지 파일에 접근할 때는 (public 없이)/img/logo.png 방식으로 접근해야 하며, Static 미들웨어는 이 파일을 전송할 때 컨텐츠 타입도 자동으로 설정해준다 미들웨어는 위에서부터 순차적으로 처리되며, 따라서 (보통 앞부분에 놓이는)Static 미들웨어가 이후에 나오는 라우트를 가로챌 수도 있으므로 혼란스러운 결과가 나오는 때는 정적 파일 중에 라우트와 충돌하는 것이 있는지 확인해볼 필요가 있다 - 예컨대, public 폴더 안에 index.html 파일이 들어있는 경우 등..

사용자 모듈

템플릿을 렌더링할 때는 템플릿 엔진에 컨텍스트를 전달하며, 이를 통해 템플릿에 데이터를 삽입한다

사용자 모듈 만들기: 특정 기능의 모듈화 및 캡슐화
사용자가 만들어 추가한 파일 모듈에 있는 내용을 모듈 바깥에서 접근할 수 있도록 module.exports를 사용하여 내보내는데, 바깥에서는 const study= require('./lib/study') 식으로 임포트해서 사용하며, study.getStudy() 식으로 외부 함수를 그대로 쓸 수 있다:
                                    
                                        /* 모듈화와 캡슐화를 위한 lib/study.js 모듈 */
                                        const studies= [ // 캡슐화: 이 부분은 바깥에서 볼 수 없다!
                                            "html 5",
                                            "Css 3",
                                            "Bootstrap 5",
                                            "JavaScript"
                                        ]
                                        
                                        exports.getStudy= () => { // 바깥에서 접근할 수 있도록 전역변수 exports 사용!
                                            const idx= Math.floor(Math.random()*studies.length)
                                            return studies[idx]
                                        }
                                    
                                

사용자가 작성한 모듈은 보통 lib 디렉토리를 만들어 저장한다!

다음으로, 웹서버 파일의 맨 위에 const study= require('./lib/study')를 추가해주고, 웹서버 파일의 about 라우트 부분도 다시 수정해준다:
                                    
                                        /* study의 모듈화 */
                                        const study= require('./lib/study') // 사용자 모듈 study 임포트
                                        
                                        const express= require('express')
                                        const app= express()
                                        
                                        const expressHandlebars= require('express-handlebars').create({ defaultLayout: 'main' })
                                        app.engine('handlebars', expressHandlebars.engine)
                                        app.set('view engine', 'handlebars')
                                        
                                        // 웹서버 포트 설정:
                                        const port= process.env.PORT || 7000
                                        
                                        // Static 미들웨어:
                                        app.use(express.static(__dirname + '/public' ))
                                        
                                        // 루트 라우팅:
                                        app.get('/', (req, res) => { res.render('home') })
                                        
                                        // /about 라우팅 수정
                                        app.get('/about', (req, res) => {
                                            res.render('about', { study: study.getStudy() })
                                        })
                                        
                                        // 커스텀 404 페이지:
                                        app.use((req, res) => {
                                            res.status(404)
                                            res.render('404 - Not Found !')
                                        })
                                        
                                        // 커스텀 500 에러 페이지:
                                        app.use((err, req, res, next) => {
                                            console.error(err.message)
                                            res.status(500)
                                            res.render('500 - Server Error !')
                                            // res.status(500).render('08-error', { message: "You shouldn't have clicked that!" })
                                        })
                                    
                                

사용자 모듈 임포트에서 ./node_modules가 아닌 lib 폴더에서 찾아 임포트하라는 의미이다 - 이 부분을 생략하면 임포트에 실패한다!

http/2 서버 구동하기

https(포트: 443)는 http 가 네트워크상에서 암호화된 버전으로서, 불가피한 경우를 제외하고는 항상 https 를 사용하여 웹사이트를 전송하는 것이 좋다! 이것은 클라이언트와 서버 간의 모든 커뮤니케이션을 암호화 하기 위하여 TLS을 사용하는데, 이러한 보안 연결은 클라이언트가 민감한 정보를 서버와 안전하게 주고받도록 해준다 - 예컨대, 금융 거래나 온라인 결재 등..

wave