이 포스팅은 노마드 코더의 "유튜브 클론코딩" 강의를 바탕으로 작성되었습니다. https://nomadcoders.co
썸네일 이미지 출처: https://developers.google.com/youtube
2021년 7월 18일
"여러분의 CS 교육에서 누락된 학기" 시리즈와 동시에 유튜브 클론코딩 시리즈를 시작하게 되었습니다. 유튜브의 핵심 기능을 클론해보면서 백엔드와 프론트엔드 간의 연결을 배우고 웹 프로그래밍에 많이 쓰이는 여러 기술들을 습득할 계획입니다. 저에게는 제대로 배우는 첫 백엔드 강의가 되겠습니다.
지난 시리즈와 마찬가지로 유튜브 클론코딩 시리즈는 자신을 위한 연습 노트가 될 것 같습니다. 강의에서 새로 배운 개념 및 깨달은 점을 중점적으로 적고, 기본적인 자바스크립트 문법은 넘어가도록 하겠습니다.
우선 프로젝트 폴더를 만들고 폴더 내부에서 $ git init
을 통해 로컬 저장소를 생성합니다. 본인이 사용하는 원격저장소와 $ git remote
로 연결합니다. 저는 GitHub를 사용합니다.
그리고 $ npm init
을 통해 package.json
파일을 생성하여 npm 을 사용할 준비도 마칩니다.
마지막으로 프로젝트 폴더 내부에 src
폴더를 생성한 후 그 안에 server.js
파일을 생성합니다. 이 파일이 지금부터 메인 파일이 됩니다.
자바스크립트의 최신 문법은 node.js 나 다른 브라우저에서 작동하지 않습니다. 따라서 프로젝트에 사용한 최신 문법을 브라우저에서 작동하는 문법으로 바꿔줄 필요가 있습니다.
$ npm i @babel/core --save-dev
로 babel을 설치합니다. 여기서 --save-dev
는 해당 모듈을 devDependencies로 분류하겠다는 의미입니다.
이렇게 프로젝트에 쓰이는 모듈을 dependencies 에, 개발 과정에서 쓰이는 모듈을 devDependencies 에 따로 분류해놓으면 나중에 package.json을 보고 어떤 모듈이 어떤 목적으로 쓰였는지 알기 쉽습니다.
babel 설치 이후 babel을 사용하기 위해서는 preset을 지정해주어야 합니다. 이 프로젝트에서는 가장 기본적인 preset인 preset-env
를 사용하겠습니다. $ npm i @babel/preset-env --save-dev
명령어로 설치해줍니다.
그리고 babel에게 해당 프리셋을 사용한다고 알려주어야 합니다. 프로젝트 폴더 최상위에 babel.config.json
파일을 만들어줍니다. 그리고 파일 안에 다음 내용을 붙여넣고 저장합니다.
{
"presets": ["@babel/preset-env"]
}
서버를 개발하면서 소스코드를 수정하고, 저장하고, 서버를 종료하고, 다시 시작하는 과정은 번거롭습니다. nodemon 은 소스코드에 변화가 생기면(변경 사항을 저장하면) 자동으로 서버를 재시작해주는 유용한 모듈입니다. $ npm i nodemon --save-dev
로 nodemon을 설치합니다.
앞서 babel이 최신 자바스크립트 문법을 브라우저가 이해할 수 있는 버전으로 바꿔준다고 했습니다. 이제 소스코드를 저장하면 babel이 코드를 컴파일하고, 그 코드를 nodemon이 실행하게 하는 연속 명령을 만들어보겠습니다.
그러기 전에 nodemon과 함께 쓰는 데 필요한 babel 모듈 하나를 더 다운받습니다. $ npm i @babel/node --save-dev
이제 package.json 파일을 열어 scripts
부분을 다음과 같이 바꿔줍니다.
"scripts": {
"dev": "nodemon --exec babel-node src/server"
}
이제 모든 준비가 끝났습니다. 앞으로 서버를 실행할 때는 $ npm run dev
라는 명령어를 입력하면 모든 작업이 자동으로 일어나게 됩니다. nodemon은 소스코드가 변경될 때마다 자동으로 서버를 재시작해주는데, 서버를 완전히 종료하려면 control + c
키를 누릅니다.
babel에 대한 더 자세한 정보는 다음 링크를 참고해주세요. https://babeljs.io
npm에서 여러 모듈을 설치했기 때문에 프로젝트 폴더 내에 node_modules
라는 폴더가 생성되었을 것입니다. 이 폴더는 다운받은 모듈과 그 모듈의 의존성 모듈을 모아놓은 것으로, package.json
과 package-lock.json
파일만 있으면 언제든지 npm에서 다운받을 수 있기 때문에 이 폴더를 원격 저장소에 업로드할 필요가 없습니다.
따라서 .gitignore
라는 이름의 파일을 프로젝트 폴더 최상단에 생성한 후 다음 내용을 붙여넣습니다.
/node_modules
.gitignore
파일 안에 적혀있는 파일은 커밋 대상에서 제외됩니다. 참고로 폴더명을 적을 때는 위와 같이 앞에 /
를 붙입니다.
이제 프로젝트를 진행하기 위한 기초 공사가 끝났습니다. 본격적인 프로젝트로 들어가기에 앞서, 프로젝트에서 사용할 프레임워크인 Express와 Express의 Router에 대한 기초를 짚고 넘어가보겠습니다.
node.js를 사용하여 자바스크립트로 서버를 구현할 수 있습니다. 그러나 아무것도 없는 바닥에서부터 서버를 구현할 필요는 없습니다. 이미 서버를 어떤 방식으로 만들지를 구상해놓은 틀을 가져다가 쓰면 됩니다. Express
는 그러한 틀 중 하나입니다.
Express를 사용하기 위해서 $ npm i express
명령어를 실행합니다.
이제 src
폴더의 server.js
파일에 서버를 만들어보겠습니다. 우선 express를 사용할 것이므로 express를 사용한다는 사실을 첫 줄에 알려줍니다.
import express from "express";
import
뒤의 express
에는 어떤 이름도 들어갈 수 있습니다. 주로 모듈 이름이 너무 긴 경우 별명을 지어 사용합니다. 여기서는 express를 그대로 사용하겠습니다.
이제 express의 뼈대를 만들어보겠습니다.
import express from "express";
const app = express(); // 1
const PORT = 4000; // 2
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
}); // 3
순서대로 설명하면 이렇습니다.
app
변수에 express 어플리케이션을 생성합니다.PORT
변수에 서버가 사용할 포트 번호를 저장합니다.이제 서버의 뼈대가 완성되었습니다. 아까 만들어두었던 dev
스크립트를 실행하기 위해 $ npm run dev
명령어를 입력합니다.
[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `babel-node src/server.js`
Server is listening on port 4000
nodemon 실행과 함께 서버가 시작되었다는 알림이 잘 출력되고 있습니다. 이제 이 서버에 연결을 시도해보겠습니다. 웹 브라우저를 열고 http://localhost:4000
을 입력합니다.
해당 주소로 접속하면 Cannot GET /
이라는 메시지만 보입니다. 해당 주소로 들어가면 브라우저는 루트 /
페이지를 서버에게 요구합니다. 그러나 우리가 만든 서버는 요청을 받기만 할 뿐 그것을 다룰 줄 모릅니다. 이제 요청을 처리하는 방법에 대해 알아보겠습니다.
app.get()
은 사용자의 요청을 처리하는 방법 중 하나입니다.
import express from "express";
const app = express();
const PORT = 4000;
app.get("/", () => {
console.log("Someone go to Home");
});
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
app.get()
을 사용해 /
(루트) 요청을 받으면 콘솔에 메시지를 출력하도록 했습니다. 이제 localhost:4000
에 접속하면 Cannot GET /
메시지가 뜨는 대신 로딩이 멈춰버립니다.
이유는 간단합니다. app.get("/")
으로 /
요청을 받았습니다. 그러나 요청에 대한 응답은 하지 않았습니다. 브라우저는 요청에 대한 응답이 올 때까지 계속 기다리게 됩니다.
사용자의 요청에 대한 응답을 보내기 위해서는 request, response에 대해 알아야 합니다. request
는 사용자의 요청에 관한 정보와 도구를 담고 있는 객체이며, response
는 보내줄 응답에 관한 정보와 도구를 담고 있는 객체입니다.
이 두 객체를 사용하기 위해서는 get()
의 콜백 함수가 이들을 인자로 받아야 합니다.
app.get("/", (req, res) => {
res.end();
});
app.get()
부분을 다음과 같이 수정합니다. 콜백 함수는 req
와 res
두 객체를 인자로 받습니다. end()
메서드를 사용하여 응답을 종료합니다. end()
는 아무런 데이터 없이 응답을 종료할 때 사용합니다. 이렇게 되면 브라우저는 무한 로딩에서 벗어날 수 있게 됩니다.
이번에는 데이터를 응답으로 보내겠습니다.
app.get("/", (req, res) => {
res.send("Hello World!");
});
이제 브라우저로 돌아가 새로고침 해보면 화면에 Hello World
가 보입니다. send()
메서드는 HTTP 응답을 보냅니다. 문자열 뿐만 아니라 json이나 HTML 형식도 전송할 수 있습니다.
app.get("/", (req, res) => {
res.send("<h1>Hello World!</h1>");
});
app.get("/", (req, res) => {
res.send({message: "Hello!"});
});
middleware는 말 그대로 가운데에 있는 무엇입니다. 여기서 가운데란 request와 response의 중간을 말합니다. 때로는 사용자의 요청을 조금 더 복잡하게 처리해야 합니다. 예를 들어, 사용자의 로그인 여부를 확인하고 정보를 보여줘야 할 때가 있습니다. middleware는 이럴 때 사용할 수 있습니다.
const handleHello = (req, res) => {
res.send("Hello");
}
const middleware = (req, res) => {
console.log("I'm middleware");
}
app.get("/", middleware, handleHello);
middleware
와 handleHello
라는 콜백 함수를 만듭니다. 그리고 두 콜백 함수를 get()
에 인자로 전달합니다. 이 상태에서 실행되는 것은 middleware
하나 뿐입니다. middleware
가 실행되고 난 이후 handleHello
를 실행할 수 있도록 연결을 만들어주어야 합니다.
const handleHello = (req, res) => {
res.send("Hello");
};
const middleware = (req, res, next) => {
console.log("I'm middleware");
next();
};
app.get("/", middleware, handleHello);
middleware
함수는 next
라는 세 번째 매개변수를 가질 수 있습니다. next
에는 get()
에 적어주었던 다음 함수가 인자로 들어가게 됩니다. 여기서는 middleware
, handleHello
순서대로 get()
에 전달하였으므로 middleware
의 next
는 handleHello
가 됩니다.
middleware
함수의 마지막에 next()
를 적어주면 handleHello
가 이어서 실행됩니다. 이런 식으로 middleware는 다음 middleware에게 차례를 넘겨줍니다.
지금까지 app.get()
을 사용해 특정 주소의 요청을 받고 미들웨어도 등록했습니다. app.use()
를 사용하면 일종의 '전역 미들웨어'를 등록할 수 있습니다. 사용자가 어떤 주소로 요청을 보내도 해당 미들웨어를 거치게 됩니다.
const handleHello = (req, res) => {
res.send("Hello");
};
const middleware = (req, res, next) => {
console.log("I'm middleware");
next();
};
app.use(middleware);
app.get("/", handleHello);
아까 만들었던 미들웨어를 app.use()
로 등록했습니다. 이렇게 되면 사용자의 모든 요청은 일단 middleware
가 처리한 후 app.get()
으로 넘어가게 됩니다.
app.use()
를 사용하는 순서는 매우 중요합니다. 만약 순서를 다음과 같이 바꾸면 middleware
는 실행되지 않습니다.
app.get("/", handleHello);
app.use(middleware);
미들웨어가 작동하기도 전에 handleHello
가 먼저 응답을 종료하기 때문입니다.
이번에는 조금 더 복잡한 미들웨어를 만들어보겠습니다.
const handleHello = (req, res) => {
res.send("Hello");
};
const prevent = (req, res, next) => {
if (req.url === "/ban") {
return res.send("Not Allowed");
}
next();
};
app.use(prevent);
app.get("/", handleHello);
prevent
미들웨어는 사용자가 특정 URL로 접근 시 응답을 종료해버립니다.
미들웨어는 만들어 쓸 수도 있지만 이미 만들어진 미들웨어를 쓰는 것도 가능합니다. morgan은 HTTP 요청 로그를 콘솔에 출력해주는 미들웨어입니다.
https://www.npmjs.com/package/morgan
morgan을 설치하기 위해
$ npm i morgan
명령어를 입력합니다.server.js
맨 윗 줄에 import morgan from "morgan";
을 작성합니다.그리고 다음과 같이 코드를 작성합니다.
const handleHello = (req, res) => {
res.send("Hello!");
};
app.use(morgan("dev"));
app.get("/", handleHello);
morgan은 미들웨어를 생성할 때 포멧을 지정해줄 수 있습니다. 여기서는 dev
로 설정합니다.
이제 사용자가 HTTP요청을 보낼 때마다 콘솔에 로그가 표시됩니다.
GET / 200 8.703 ms - 6
요청 메소드, url, 상태, 응답에 소요된 시간 등이 표시됩니다.
아직까지 서버는 /
url에 대해서만 응답할 수 있습니다. 다음 포스팅에서는 Express의 router에 대해 알아보겠습니다.
<참고 문서>