당연히 현재 미들웨어에서 응답을 보내는 경우, 즉 res.send() 나 res.json() 등의 메서드를 호출하는 경우 next()를 호출하면 안된다. 이렇게 하지 않으면 이미 요청이 종료된 상태에서 다른 미들웨어가 응답을 보내려고 해서 중복된 요청이 전달되는 문제가 발생한다.
라우터와 미들웨어 차이
라우터와 미들웨어는 서로 다른 방식처럼 보이지만 라우터는 미들웨어 기반으로 구현된 객체라서 미들웨어와 동일한 방식으로 작동된다.
라우터는 미들웨어 함수를 특정 경로에 바인딩하는 역할을 하고, 요청이 들어온 URL 경로에 따라 서로 다른 미들웨어를 실행시킬 수 있게 도와준다.
app.use(Middleware) : 모든 요청에서 미들웨어가 실행된다.
app.use('/api', Middleware) : /api로 시작하는 모든 요청에서 미들웨어를 실행한다.
app.post('/api', Middleware, (req,res)=>{}) : /api로 시작하는 POST 요청에서 미들웨어를 실행한다.
Request의 세부 사항에는 URL, http method, 헤더( header ), 쿼리 파라미터 ( query parameter ), 바이 데이터 ( body data ) 등이 포함 된다.
Response
Response란 서버에서 클라이언트로 응답 메세지를 전송시켜주는 객체다.
Response의 세부 사항에는 상태 코드 ( status code ), 응답 데이터 ( response data ), 응답 헤더 ( response header ) 등이 포함된다.
서버 모듈
Node.js의 서버 모듈에는 대표적으로 http 모듈과 Express.js가 존재한다. ( http 모듈은 Node.js에서 기본 제공하는 http 서버 모듈이다 )
Express.js는 http 모듈을 확장해 제공한다.
Express.js는 기존 http 모듈의 메서드도 사용할 수 있지만, Express.js가 추가 제공하는 메서드나 속성들을 사용할 수 있다.
Express.js 통신 흐름
클라이언트는 특정 URL과 데이터를 담은 요청 ( Request )을 서버에 전송한다.
서버는 받은 데이터에 따라 필요한 비즈니스 로직을 수행한다.
서버는 처리된 결과를 클라이언트에게 응답 ( Response )으로 보내준다.
예제 )
app.js
// app.js
import express from 'express';
import goodsRouter from './routes/goods.js';
import newsRouter from './routes/news.js';
const app = express();
const PORT = 3000; // 서버를 열 때 사용할 포트 번호
app.get('/', (req, res) => {
res.send('Hello World!');
});
// 1. Express.js의 서버를 엽니다.
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
// localhost:3000/api -> goodsRouter
// localhost:3000/api -> newsRouter
// 2. 라우터를 등록 합니다.
// app.use 미들웨어를 사용해 /api로 접속하면 goodsRouter와 newsRouter를 연결(등록)한다
app.use('/api', [goodsRouter, newsRouter]);
1. 서버시작
app.listen을 통해 3000 포트로 웹 서버를 연다.
2. 라우터 등록
app.use 미들웨어를 사용해 /api 주소에 goodsRouter와 newRouter를 연결(등록)한다.
news.js
// routes/news.js
import express from 'express';
const router = express.Router();
// req : 클라이언트가 전달한 Path Params
// res : 서버가 클라에게 전달할 데이터 ( json 형태로 전달 )
/** 뉴스 목록 조회 API **/
// 3. HTTP Method와 URL을 지정한 API를 정의합니다.
// 만약, localhost:3000/api/news 라는 URL로 GET 요청이 들어온다면 해당 코드를 실행합니다.
router.get('/news', (req, res) => {
// 4. 사용자의 요청에 맞는 데이터를 반환합니다.
return res // Express.js의 res 객체를 반환합니다.
.status(200) // API의 상태 코드를 200번으로 전달합니다.
.send('뉴스 목록 조회 API 입니다.'); // API의 결과값을 '뉴스 목록 조회 API 입니다.'로 전달합니다.
});
/** 뉴스 세부 조회 API **/
// 3. HTTP Method와 URL을 지정한 API를 정의합니다.
// 만약, localhost:3000/api/news/:newsId 라는 URL로 GET 요청이 들어온다면 해당 코드를 실행합니다.
router.get('/news/:newsId', (req, res) => {
// 클라이언트가 전달한 Path Params 데이터를 받아옵니다.
const params = req.params;
// Path Params 데이터 중 newsId를 추출합니다.
const newsId = params.newsId;
// 서버 콘솔에 클라이언트가 전달한 newsId를 출력합니다.
console.log('클라이언트로 부터 전달받은 뉴스 ID:', newsId);
// 4. 사용자의 요청에 맞는 데이터를 json 형태로 반환합니다.
return res.status(200).json({
data: '뉴스 세부 조회 API 입니다.',
});
});
// Express 라우터를 외부로 전달합니다.
export default router;
3. API 정의
등록된 각 라우터를 순서대로 검토하고, HTTP Method와 URL이 일치하는 함수를 실행한다.
-URL /api/news로 GET 요청이 들어오면 router.get('/new' ~ )을 실행한다.
-URL /api/news/:newsId로 GET 요청이 들어오면 router.get('/new/:newsId' ~)을 실행한다.
여기서 중요한 부분은 :newId에 해당하는 경로 변수다. 이는 req.params을 통해 들어온 변수를 읽어들일 수 있다.
위 코드에서는 req.params을 통해 들어온 변수를 params에 저장하고 params.newsId를 통해 해당 변수를 읽어오고 출력해주는 형태다
모듈은 대개 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성된다.
모듈의 필요성
코드 베이스를 분리할 수 있고, 이를 통해 코드를 구조적으로 관리할 수 있다.
코드를 재사용 가능하게 만들어준다. ( = 모듈화 )
코드의 함수와 변수중 일부만 외부에서 사용하도록 노출시킬 수 있다. ( 즉, 모듈 내부의 코드 세부사항을 외부로 부터 은닉하는 정보은닉을 표현할 수 있다. )
해당 모듈이 참조하고 있는 다른 모듈에 대한 종속성을 관리하는 역할을 담당한다.
CJS ( CommonJS )
Node.js 환경에서 기본적으로 사용되는 모듈 시스템이다.
설정을 따로 추가하지 않은 이상, Node.js에서는 CommonJS를 기본으로 사용한다.
require 함수를 사용하여 다른 모듈을 불러올 수 있다.
require 함수는 경로 혹은 문자열을 가지고 내부 알고리즘을 통해 모듈을 가져오고 종속성을 처리한다.
ESM ( ECMA Script Module )
최신 Javascript에서 지원하는 모듈 시스템이다.
모든 Javascript 환경에서 통합적인 인터페이스를 제공하기 위해 시작된 체계다.
CommonJS 와는 다르게 정적으로 모듈을 가져오며 비동기적 모듈 로딩과 순환 종속을 처리한다.
모듈의 사용법
모듈 사용법 ( 출처 : hacks.mozilla.org )
export 명령어를 변수나 함수 앞에 붙이면 외부 모듈에서 해당 변수나 함수에 접근할 수 있다.
import, require 명령어를 사용해 외부 모듈의 기능을 가져올 수 있다.
● import는 ES6( ES2015 )로 모듈 시스템을 관리할 때 사용한다.
이 방식은 정적 로딩을 지원한다.
import 문은 코드의 최상위에 위치해야 한다
● require는 CommonJS로 모듈시스템을 관리할 때 사용한다.
이 방식은 동적 로딩을 지원한다.
require 문은 코드의 어디에서든 사용할 수 있다.
ES6 Module 사용
// 📁 sayHi.js
export function sayHi(user) {
console.log(`Hello, ${user}!`);
}
export 지시자를 사용해 sayHi.js 내부의 함수 sayHi를 외부로 내보낸다.
// 📁 main.js
import {sayHi} from './sayHi.js';
console.log(sayHi); // function
sayHi('John'); // Hello, John!
import 지시자를 사용해 상대 경로 ( ./sayHi.js ) 기준으로 모듈을 가져오고 sayHi.js 에서 내보낸 함수 sayHi를 상응하는 변수에 할당한다.
상대 경로
import {sayHi} from './sayHi.js';
위 코드에서 ./sayHi는 파일 경로를 나타내는데, ./ 으로 시작하는 경로는 '상대 경로' 라는 것을 나타낸다.
여기서, '상대 경로'란 현재 파일의 위치에 따라 다르게 해석되는 파일 또는 디렉토리의 위치는 나타내는 방식이다.
즉, ./sayHi.js 는 현재 파일이 위치한 디렉토리에서 sayHi.js라는 이름의 파일을 찾는 것을 의미한다.
상위 디렉토리에서 파일을 찾으려면 ../ 을 이용하면 된다.
즉, ../sayHi.js 라고 상대경로를 설정하면 현재 파일이 위치한 디렉토리의 상위 디렉토리에서 sayHi.js 를 찾는 것을 의미한다.
화살표 함수 export, import
/** 화살표 함수 export **/
// 모듈을 호출했을 때, addArrowFunction 키 값에는 addArrowFunction 변수 함수가 가지고 있는 값이 할당된다.
export const addArrowFunction = (a, b) => {
return a + b;
}
/** 화살표 함수 import **/
import { addArrowFunction } from './math.js'
console.log(addArrowFunction(5, 3));
// Print: 8
숫자형엔 일반적인 숫자 외에 Infinity, -Infinity, NaN 같은 특수 숫자 값이 포함된다.
Infinity는 무한대를 나타낸다.
일반적으로 0으로 나누면 무한대를 얻을 수 있다.
console.log( 1 / 0 );
console.log( Infinity );
NaN은 계산 중 에러가 발생했다는 것을 나타내주는 값이다.
부정확하거나 정의되지 않은 수학 연산을 사용하면 계산 중 에러가 발생하는데, 이때 NaN이 반환된다.
console.log( "글자" / 2 ); // NaN
NaN은 웬만해선 바뀌지 않는다. NaN에 어떤 추가 연산을 해도 결국 NaN이 반환된다.
BigInt
자바스크립트에선 내부 표현 방식 때문에 (2^53 - 1)(9007199254740991) 보다 큰 값 또는
-(2^53-1) 보다 작은 정수는 숫자형을 사용해 나타낼 수 없다.
BigInt 형은 길이에 상관없이 정수를 나타낼 수 있다.
BigInt 형 값은 정수 리터럴 끝에 n을 붙이면 만들 수 잇다.
// 끝에 'n'이 붙으면 BigInt형 자료입니다.
let bigInt = 1234567890123456789012345678901234567890n;
문자형
자바스크립트에선 문자열을 따옴표로 묶는다.
let str = "Hello";
let str2 = 'Single quotes are ok too';
let phrase = `can embed another ${str}`;
따옴표는 세 종류가 있다.
1. 큰따옴표 : "Hello"
2. 작은따옴표 : 'Hello'
3. 역 따옴표( 백틸, backtick ) : `Hello`
큰따옴표와 작은따옴표는 기본적인 값으로, 자바스크립트에서는 이 둘에 차이를 두지 않는다.
역 따옴표로 변수나 표현식을 감싼 후 ${...} 안에 넣어주면, 다음과 같이 원하는 변수나 표현식을
문자열 중간에 손쉽게 넣을 수 있다.
let name = "John";
// 변수를 문자열 중간에 삽입
console.log( `Hello, ${name}!` ); // Hello, John!
// 표현식을 문자열 중간에 삽입
console.log( `the result is ${1 + 2}` ); // the result is 3
Boolean 형
boolean형은 ture와 false 두 가지 값밖에 없는 자료형 이다.
boolean형은 긍정이나 부정을 나타내는 값을 저장할 때 사용한다.
let nameFieldChecked = true;
let ageFieldChecked = false;
null 값
null 값은 지금까지 소개한 자료형 중 어느 자료형에도 속하지 않는 값이다.
null 값은 오로지 null 값만 포함하는 별도의 자료형이다.
let age = null;
자바스크립트의 null은 다른 언어에서의 null과는 성격이 다르다.
다른 언어에서는 null을 존재하지 않는 객체에 대한 참조나 널 포인터를 나타낼 때 사용한다.
하지만 자바스크립트에서 null은 존재하지 않는 값, 비어 있는 값, 알 수 없는 값을 나타내는 데 사용한다.
위 예시를 들어 설명하자면 let age = null; 은 나이를 알 수 없거나 그 값이 비어있음을 보여준다. ( 해석의 영역 )
undefined 값
undefined 값도 null 값처럼 자신만의 자료형을 형성한다.
undefined는 값이 할당되지 않은 상태를 나타낼 때 사용한다.
변수는 선언했으나, 값을 할당하지 않았다면 해당 변수에 undefined가 자동으로 할당된다.
let age;
console.log(age); // 'undefined'가 출력됩니다.
변수에 undefined를 명시적으로 할당하는 것도 가능하다.
let age = 100;
// 값을 undefined로 바꿉니다.
age = undefined;
console.log(age); // "undefined"
하지만 이렇게 undefined를 직접 할당하는 것은 권장되지 않는다.
변수가 비어있거나 알 수 없는 상태라는 걸 나타낼 때는 null을 사용하는 것이 좋다.
undefined는 값이 할당되지 않은 변수의 초기값을 위해 예약어로 남겨두는 편이 낫다.
typeof 연산자
typeof 연산자는 인수의 자료형을 반환한다. 자료형에 따라 처리 방식을 다르게 하고 싶을 경우와
Array.prototype엔 요소 사이에 쉼표를 넣어 요소 전체를 합친 문자열을 반환하는 자체 메서드 toString이 있다.
let arr = [1, 2, 3]
console.log(arr); // 1,2,3 <-- Array.prototype.toString의 결과
그런데 Object.prototype에도 메서드 toString이 있다. 이렇게 중복 메서드가 있을 때는 체인 상에서 가까운 곳에 있는 메서드가 사용된다. Array.prototype 이 체인 상에서 더 가깝기 때문에 예시에서는 Array.prototype의 toString이 사용된다.
Chrome 개발자 콘솔과 같은 도구를 사용해 console.dir를 사용하면 내장 객체의 상속 관계를 확인할 수 있다.
배열이 아닌 다른 내장 객체들 또한 같은 방법으로 동작한다.
함수도 마찬가지며 call, apply를 비롯한 함수에서 사용할 수 있는 메서드는 Function.prototype에서 받아온다.
class MyClass {
// 여러 메서드를 정의할 수 있음
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
위처럼 클래스를 만들고 new Myclass()를 호출하면 객체가 생성된다.
constructor()는 생성자로 new에 의해 자동으로 호출되고, 객체의 기본 상태를 설정해준다.
class User {
constructor(name) {
this.name = name;
}
sayHi() {
console.log("안녕하세요");
}
}
// 사용법:
let user = new User("Yam");
user.sayHi();
위처럼 new User("Yam")를 호출하면, 새로운 객체가 생성되고,
넘겨받은 매개변수와 함께 constructor가 자동으로 실행된다.
자바스크립트에서 클래스는 함수의 한 종류로 취급한다.
class User {
constructor(name) { this.name = name; }
sayHi() { console.log(this.name); }
}
// User가 함수라는 증거
console.log(typeof User); // function
class User 문법 구조가 하는일은 다음과 같다.
1. User라는 이름을 가진 함수를 만들고, 함수 본문은 constructor에서 가져온다.
constructor가 없으면 본문이 비워진 채로 함수가 만들어진다.
2. sayHi와 같은 메서드를 User.prototype에 저장한다.
new User를 호출해 객체를 만들고, 객체의 메서드를 호출하면 메서드를 prototype 프로퍼티를 통해 가져온다.
이 과정이 있어서 객체에서 메서드에 접근할 수 있다.
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// 클래스는 함수다.
console.log(typeof User); // function
// 정확히는 생성자 메서드와 동일하다.
console.log(User === User.prototype.constructor); // true
// 클래스 내부에서 정의한 메서드는 User.prototype에 저장된다.
console.log(User.prototype.sayHi); // console.log(this.name);
// 현재 프로토타입에는 메서드가 두 개다.
console.log(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
class라는 키워드 없이도 클래스 역할을 하는 함수를 선언할 수 있다.
// class User와 동일한 기능을 하는 순수 함수를 만들어보자.
// 1. 생성자 함수를 만든다.
function User(name) {
this.name = name;
}
// 모든 함수의 프로토타입은 'constructor' 프로퍼티를 기본으로 갖고 있기 때문에
// constructor 프로퍼티를 명시적으로 만들 필요가 없다.
// 2. prototype에 메서드를 추가한다.
User.prototype.sayHi = function() {
console.log(this.name);
};
// 사용법:
let user = new User("John");
user.sayHi();
위 처럼 순수 함수로 클래스 역할을 하는 함수를 선언하는 방법과 class 키워드를 사용하는 방법의 결과는 거의 같다.
이 두 방법에는 중요한 차이가 몇가지 있다.
1. class로 만든 함수엔 특수 내부 프로퍼티인 IsClassConstructor : true 가 이름표처럼 붙는다.
이것만으로도 두 방법엔 분명한 차이가 있음을 알 수 있다.
class User {
constructor() {}
}
console.log(typeof User); // User의 타입은 함수이긴 하지만 그냥 호출할 수 없다
User(); // TypeError: Class constructor User cannot be invoked without 'new'
자바스크립트는 다양한 경우 IsClassConstructor : true를 활용한다.
클래스 생성자를 new와 함께 호출하지 않으면 에러가 발생하는데 이 때 IsClassConstructor : true 가 사용된다.
또 다른 차이점으로는
2. 클래스에 정의된 메서드는 열거할 수 없다.
클래스의 prototype 프로퍼티에 추가된 메서드의 enumerable 플래그는 false 이기 때문.
for in 으로 객체를 순회할 때, 메서드는 순회 대상에서 제외하고자 하는 경우가 많아 이 특징은 꽤 유용하다.
3. 클래스는 항상 엄격 모드로 실행된다. 클래스 생성자 안 코드 전체에 자동으로 엄격 모드가 적용된다.
클래스 표현식
함수처럼 클래스도 다른 표현식 내부에서 정의, 전달, 반환, 할당할 수 있다.
클래스 표현식을 만들어보자.
let User = class {
sayHi() {
console.log("안녕하세요.");
}
};
기명 함수 표현식과 유사하게 클래스 표현식에도 이름을 붙일 수 있다.
클래스 표현식에 이름을 붙이면, 이 이름은 오직 클래스 내부에서만 사용할 수 있다.
// 기명 클래스 표현식(Named Class Expression)
// (명세서엔 없는 용어이지만, 기명 함수 표현식과 유사하게 동작한다.)
let User = class MyClass {
sayHi() {
console.log(MyClass); // MyClass라는 이름은 오직 클래스 안에서만 사용할 수 있다.
}
};
new User().sayHi(); // 원하는대로 MyClass의 정의를 보여준다.
console.log(MyClass); // ReferenceError: MyClass is not defined, MyClass는 클래스 밖에서 사용할 수 없다.
getter와 setter
클래스는 getter와 setter를 지원한다.
get과 set을 이용해 user.name을 조작해보자!
class User {
constructor(name) {
// setter를 활성화합니다.
this.name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 4) {
console.log("이름이 너무 짧습니다.");
return;
}
this._name = value;
}
}
let user = new User("성원");
console.log(user.name); // 성원
user = new User(""); // 이름이 너무 짧습니다.
참고로 getter와 setter는 User.prototype에 정의된다.
클래스 필드
클래스 필드라는 문법을 사용하면 어떤 종류의 프로퍼티도 클래스에 추가할 수 있다.
클래스 User에 name 프로퍼티를 추가해보자.
class User {
name = "성원";
sayHi() {
console.log(`${this.name}님 안녕하세요!`);
}
}
new User().sayHi(); // 성원님 안녕하세요!
클래스를 정의할 때 <프로퍼티 이름> = <값> 을 써주면 간단히 클래스 필드를 만들 수 있다.
클래스 필드의 중요한 특징 중 하나는 User.prototype이 아닌 개별 객체에만 클래스 필드가 설정된다는 점이다.
class User {
name = "성원";
}
let user = new User();
console.log(user.name); // 성원
console.log(User.prototype.name); // undefined
Set은 원시 값이나 객체 참조 값 등 모든 유형의 고유 값을 저장할 때 사용하는 객체다.
선언
let mySet = new Set();
add ( 데이터 넣기 )
Set에 데이터를 넣는다. 해당 값이 중복되면 데이터가 저장되지 않는다.
let mySet = new Set();
// 데이터 넣기
mySet.add(1);
mySet.add(2);
mySet.add(2); // 데이터가 중복되므로 저장되지 않는다.
delete ( 데이터 삭제 )
Set에서 데이터를 삭제한다. 데이터 삭제에 성공하면 true를 반환하고 실패하면 false를 반환한다.
let mySet = new Set();
// 데이터 넣기
mySet.add(1);
mySet.add(2);
// 데이터 삭제하기
mySet.delete(1); // true
mySet.delete(3); // false
clear ( 데이터 모두 삭제 )
Set에 저장되어 있는 데이터 전부를 삭제한다. 삭제에 성공하면 true, 실패하면 false를 반환한다.
let mySet = new Set();
// 데이터 넣기
mySet.add(1);
mySet.add(2);
// 데이터 모두 삭제하기
mySet.clear(); // true
Set 순회하기
let mySet = new Set();
mySet2.add(1);
mySet2.add(2);
// 데이터 순회하기
for (let item of mySet) {
console.log(item);
}
for (let item of mySet.values()) {
console.log(item);
}