블로그용 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
-devpackage.jsondevDependencies로 포함되며 개발환경과 배포환경을 구분하기 위한 시멘틱 버전의 일종이다.

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

// getting-started.js
const mongoose = require('mongoose');

main().catch(err => console.log(err));

async function main() {
  await mongoose.connect('mongodb://localhost:27017/test');
}

6버전부터 newURLparser,useUnifiedTopology를 명시할 필요가 없어졌다.

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.jsapp.use(express.json());를 추가해주면
서버에서 json파일과 객체를 보낼 수 있게된다.

입력했던 객체값과 같이 몽구스에서 new Schema로 생성했을때 같이 생성되는 _id값과 createdAt, updatedAt와 같은 타임스템프도 나오는것을 확인 할 수 있다.

클러스터의 Collections를 확인해 보면 MongoURL의 데이터베이스 이름(blog)을 부모로 users라는 데이터베이스가 들어와있는걸 확인할 수 있다.

bcrypt로 패스워드 암호화하기

응답값을 보면 password 가 그대로 드러나는 문제점이 있다.
bcrypt를 사용해서 암호화 할 수 있다.
npm install bcrypt

bcrypt | npm
Usage

async (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 | StackOverFlow

Mongoose v6.2.9 APIdocs | Model.findOne

MongoDB - Find a document

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> };

패스워드만 빼고 나머지 정보들로만 클라이언트에게 응답할 수 있게 되었다.

좋은 웹페이지 즐겨찾기