블로그용 API 만들기 -1
Photo by NordWood Themes on Unsplash
다음 영상을 바탕으로 작성된 글입니다.
Node.js Blog App REST API with MongoDB | Lama Dev
노드가 없다면 노드를 설치하자 LTS 버전을 사용하면 된다.
vscode를 실행하고 새 터미널을 실행한다.
만들고 싶은 프로젝트 이름으로 적당한 폴더를 하나 만들고
cd 폴더이름 으로 이동한다.
다음을 입력한다.
npm init
npm을 초기화한다. 패키지 정보에 관한 각종 질문을 던지는데 부담스럽다면 첫번째 질문인 프로젝트 이름만 정하고, 죄다 엔터만 치거나, npm init -y
로 실행해도 된다.
npm install express
express js를 설치한다. nodejs 라우팅 작업을 간편하게 해준다.
npm install mongoose --save
몽구스를 설치한다. 몽고DB에 스키마를 적용할 수 있다.
npm install dotenv --save
dotenv를 설치한다. 숨겨할 값이나 변수같은 환경설정을 저장할 수 있다.
npm install --save multer
파일 전송을 쉽게 해주는 미들웨어다
설치 후 package
index 만들기
express http 서버 테스트해보기
Express "Hello World" example
package.json 의 "script"
에 "start":"node index.js"
로 명령어를 추가해준다.
npm start
로 앱 실행해본다
개발환경을 위한 nodemon 설치하기
파일이 변경점이 있을때 자동으로 재시작해줌
npm install --save-dev nodemon
-dev
는 package.json
의 devDependencies
로 포함되며 개발환경과 배포환경을 구분하기 위한 시멘틱 버전의 일종이다.
Specifying dependencies and devDependencies in a package.json file
- "dependencies": Packages required by your application in production.
- "devDependencies": Packages that are only needed for local development and testing.
start
명령어를 nodemon index.js
로 바꿔서 nodemon으로 실행되게 해도되고
나는 그냥 test
명령어에 넣었음
이제 listen
밖에서도 console.log를 볼 수 있다.
app.use(middlewarefunction)
미들웨어를 불러올때 사용한다.
앞으로 만들 라우터 함수를 불러오는데 쓰일 예정임
dotenv 사용하기
.env 파일 만들기
외부로 노출되어서는 안될 키값이나 url, 함수를 저장해놓을 수 있다.
app.use
로 불러와야 할 함수들(로그인,프로필,글쓰기...등등)을 모아놓을 route를 만든다.
env 파일안에 필요한 변수(MongoURL)만들기
MongoDB 클러스터와 연결할 수 있는 URL 값을 env에 비공개로 저장할 수 있다.
MongoDB Atlas홈페이지에서 DataBase -> Overview -> 오른쪽 끝에 connect 버튼을 눌러서 확인 할 수 있다.
Connect your application을 선택한다.
안내에 따라 <password>
는 설정해놓은 비밀번호로 바꾸고 .env파일 안에
MONGO_URL = mongodb+srv://myid:비밀번호@cluster0.zsltd.mongodb.net/저장될데이터베이스이름?retryWrites=true&w=majority
이런식으로 설정해놓으면 된다.
mongoose 연결하기
// getting-started.js const mongoose = require('mongoose'); main().catch(err => console.log(err)); async function main() { await mongoose.connect('mongodb://localhost:27017/test'); }
mongoose.connect(process.env.MONGO_URL)
.then(console.log("몽고DB연결됨"))
.catch((err) => console.log(err));
인증에러발생시 콘솔
연결 성공
스키마 만들기
어떤 형식으로 값을 받고 응답해줄건지 정하는 시간이다.
몽구스로 몽고모델을 만들기 위해 다음과 같이 구조를 짠다.
User.js
부터 작성한다.
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({})
Schema
안에 원하는 객체를 작성하면 된다.
const UserSchema = new mongoose.Schema({
username:{
type: String,
required: true,
unique: true
}
})
type
: 해당 데이터의 타입
required
: 해당 프로퍼티(username)에 값을 반드시 가지고 있어야 함을 나타냄
unique
: index를 부여해서 똑같은 이름을 가진 프로퍼티를 만들지 못하도록 한다.
필요한것 : 사용자 이름(username), 이메일(email), 비밀번호(password) ,프로필사진(profileImg)
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({
username:{
type: String,
required: true,
unique: true
},
email:{
type: String,
required: true,
unique: true
},
password:{
type: String,
required: true,
},
profileImg:{
type: String,
default:"",
},
})
- password는 중복 돼도 괜찮으니 unique가 필요없다.
- profileImg는 꼭 필요하진 않기 때문에 required를 빼고, default 값을 빈 값으로 해둔다.
추가로 timestamps:true를 추가하면 자동으로 시간 템플릿을 생성해준다.(작성시간 등등)
const mongoose = require("mongoose");
module.exports = mongoose.model("User", UserSchema);
회원가입 라우터 작성하기
routes/auth.js로 간다.
const router = require("express").Router();
express의 Router 메서드로 라우터를 만든다.
모델폴더에서 유저스키마를 불러온다.
const User = require("../models/User")
회원가입은 계정을 생성해야하므로 post
메서드를 사용한다.
또한 서버와 클라이언트사이의 통신은 duration이 얼마나 걸릴지 모르므로 비동기로 작업한다.
router.post("/register", async(req, res)
req
는 서버에 들어올(요청 받을) 데이터를 말한다. 앞서 작성했던 model 형식으로 요청을 받는다.
res
는 서버가 응답해줄 데이터를 말한다. 요청 받은 데이터를 줄 수 도 있고 에러나 메세지를 줄 수 도 있다.
따라서 try...catch
로 문으로 에러를 제어한다.
router.post("/register", async (req,res) => {
try {
const newUser = new User({
username: req.body.username,
email: req.body.email,
password: req.body.password,
})
} catch (err) {
res.status(500).json(err);
}
})
const newUser = new User (req.body)
로 받을 경우
body에 형식에 맞지 않는 데이터가 들어올 수 있기 때문에 model서식에 맞게 작성한다.
const user = await newUser.save();
인수로 받은 document의 isNew 값이 true면 db에 새 document를 만들어서 저장하고 아니라면 updateOne()메서드로 기존db를 업데이트한다.
document.prototype.save()
res.status(200).json(user);
응답이 성공(200)했다면 user
데이터를 보여준다.
module.exports = router;
마지막으로 라우터로 exports 해준다.
요청보내기와 응답확인 (POSTMAN)
포스트맨에서 create new를 눌러서 HTTP Request를 선택한다.
세이브에서 Save As를 선택해서 blog
는 그룹을 만든다
Request name을 register라 정하고 blog 안에서 Save한다.
이제 정해놓은 port
번호로 요청을 보내면 된다.
index.js로 돌아가서 export한 라우터를 가져온다.
app.use()
메서드로 미들웨어로 가져온다.
//index.js
const authRoute = require("./routes/auth");
app.use('/api/auth', authRoute)
다시 포스트맨에서 해당 라우터주소로 요청을 보낸다.
서버에서 아직 body나 Header에 데이터를 넣어서 보내주는 기능이 없기 때문에 다음과 같이 오류가 발생한다.
index.js
에 app.use(express.json());
를 추가해주면
서버에서 json파일과 객체를 보낼 수 있게된다.
입력했던 객체값과 같이 몽구스에서 new Schema로 생성했을때 같이 생성되는 _id
값과 createdAt
, updatedAt
와 같은 타임스템프도 나오는것을 확인 할 수 있다.
클러스터의 Collections를 확인해 보면 MongoURL의 데이터베이스 이름(blog)을 부모로 users라는 데이터베이스가 들어와있는걸 확인할 수 있다.
bcrypt로 패스워드 암호화하기
응답값을 보면 password 가 그대로 드러나는 문제점이 있다.
bcrypt를 사용해서 암호화 할 수 있다.
npm install bcrypt
bcrypt | npm
Usageasync (recommended) const bcrypt = require('bcrypt'); const saltRounds = 10; const myPlaintextPassword = 's0/\/\P4$$w0rD'; const someOtherPlaintextPassword = 'not_bacon';
To hash a password:
Technique 1 (generate a salt and hash on separate function calls):bcrypt.genSalt(saltRounds, function(err, salt) { bcrypt.hash(myPlaintextPassword, salt, function(err, hash) { // Store hash in your password DB. }); });
Technique 2 (auto-gen a salt and hash):
bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) { // Store hash in your password DB. });
Note that both techniques achieve the same end-result.
To check a password:
// Load hash from your password DB. bcrypt.compare(myPlaintextPassword, hash, function(err, result) { // result == true }); bcrypt.compare(someOtherPlaintextPassword, hash, function(err, result) { // result == false });
auth.js 에서 REGISTER의 router.post 부분을 이렇게 고친다.
try {
const saltRounds = 10;
const salt = await bcrypt.genSalt(saltRounds);
const hashedPass = await bcrypt.hash(req.body.password, salt);
const newUser = new User({
username: req.body.username,
email: req.body.email,
password: hashedPass,
})
const user = await newUser.save();
res.status(200).json(user);
수정 후 포스트맨으로 다시 패스워드를 확인해본다.
password가 암호화 되어 누군가가 password에 접근해도 쉽게 값을 알아낼 수 없다.
MongoDB에서도 암호화되어 기록된 걸 볼 수 있다.
로그인 라우터 작성하기
회원가입과 같은 형식에 아이디/비밀번호 유효성 검사코드를 추가한다.
//LOGIN
router.post("/login", async (req, res) => {
try {
const user = await User.findOne({ username: req.body.username})
!user && res.status(400).json("아이디 혹은 비밀번호가 틀렸습니다.(사실 아이디가 틀림)")
const validated = await bcrypt.compare(req.body.password, user.password)
!validated && res.status(400).json("아이디 혹은 비밀번호가 틀렸습니다.(사실 비밀번호가 틀림)")
res.status(200).json(user);
} catch (err) {
res.status(500).json(err);
}
})
findOne()
{username: req.body.username}
이 들어간 Document
(User)를 찾는다.
!user && res.status(400)
만약 찾는 아이디가 없다면(false)
json메시지로 "아이디 혹은 비밀번호가 틀렸다"고 말해라
bycrypt.compare(req.body.password,user.password)
bycrypt의 compare 메서드로 클라이언트가 보낸 비밀번호(req.body.password)와 DB에 기록되어있는 password가 같은지 비교하고 같다면 true 틀리거나 없다면 json메세지로 "아이디 혹은 비밀번호가 틀리다"고 말해라
포스트맨으로 확인해보자.
-
아이디가 틀렸을 때
-
비밀번호가 틀렸을 때
-
로그인 성공시
패스워드 정보만 빼고 응답하기
router.post("/login", async (req, res) => {
try {
const user = await User.findOne({username: req.body.username});
!user && res.status(400).json("아이디 혹은 비밀번호가 틀렸습니다.(사실 아이디가 틀림)");
const validated = await bcrypt.compare(req.body.password, user.password);
!validated && res.status(400).json("아이디 혹은 비밀번호가 틀렸습니다.(사실 비밀번호가 틀림)");
const { password, ...나머지정보들 } = user;
res.status(200).json(나머지정보들);
} catch (err) {
res.status(500).json(err);
}
})
User에서 직접 프로퍼티 키에 접근할 수 없기 때문에
유효성 검사 뒤에 password
키와 password
를 제외한 나머지 키들로 객체 디스트럭처링 할당을 실시한다.
const { password, ...나머지들 } = user
이후 포스트맨으로 정상적인 로그인 요청을 보내면 Mongoose document class가 반환된다.
{
"$__": {
"activePaths": {
"paths": {
"password": "init",
"email": "init",
"username": "init",
"profileImg": "init",
"_id": "init",
"createdAt": "init",
"updatedAt": "init",
"__v": "init"
},
"states": {
"ignore": {},
"default": {},
"init": {
"_id": true,
"username": true,
"email": true,
"password": true,
"profileImg": true,
"createdAt": true,
"updatedAt": true,
"__v": true
},
"modify": {},
"require": {}
},
"stateNames": [
"require",
"modify",
"init",
"default",
"ignore"
]
},
"strictMode": true,
"skipId": true,
"selected": {},
"fields": {},
"exclude": null,
"_id": "6246fa2d78607288a7c11ea5"
},
"$isNew": false,
"_doc": {
"_id": "6246fa2d78607288a7c11ea5",
"username": "패스워드맨",
"email": "[email protected]",
"password": "$2b$10$fE/nUvLLC9a5D23SqeaE8eELWq/2Q0v8tEfOFAW37zzBQMsRvndwq",
"profileImg": "",
"createdAt": "2022-04-01T13:12:14.003Z",
"updatedAt": "2022-04-01T13:12:14.003Z",
"__v": 0
}
}
이중에서 _doc
프로퍼티를 살펴보면 유저 정보가 그대로 들어있는걸 알 수 있다.
디스트럭처링 할당 대상을 user
에서 user._doc
으로 바꾸고
나머지정보들
키들의 정보만 응답값으로 받으면 password
키값은 빠지게 된다.
const { password, ...나머지정보들 } = user._doc;
res.status(200).json(나머지정보들);
findOne
Destructuring of the object returned by mongodb query result | StackOverFlowMongoose v6.2.9 APIdocs | Model.findOne
https://mongodb.github.io/node-mongodb-native/4.4/modules.html#WithId
https://github.com/mongodb/node-mongodb-native/blob/b578d89/src/mongo_types.ts#L34
/* Add an _id field to an object shaped type @public /
export type WithId<TSchema> = EnhancedOmit<TSchema, '_id'> & { _id: InferIdType<TSchema> };
패스워드만 빼고 나머지 정보들로만 클라이언트에게 응답할 수 있게 되었다.
Author And Source
이 문제에 관하여(블로그용 API 만들기 -1), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@mhnormal/블로그용-API-만들기-1저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)