Search
Duplicate

Node

(feat. John Ahn)

주요개념

노드는 크롬 V8 엔진으로 빌드된 자바스크립트 런타임. 노드는 이벤트 기반, 논블로킹 I/O 모델을 사용해 가볍고 효율적임.

1. Node.js vs Express.js

Node.js: JavaScript의 런타임 환경의 일종으로, 브라우저 외에서도 자바스크립트를 사용할 수 있음
Express.js: Node.js를 편리하게 사용할 수 있는 Web Application Framework

2. V8과 libuv

V8: 구글에서 만든 자바스크립트 엔진으로 일종의 인터프리터.
libuv 라이브러리: 이벤트 루프를 기반으로 비동기 I / O를 지원하는 다중 플랫폼 C 라이브러리

3. 자바스크립트 런타임

런타임: 프로그램 실행 환경
노드는 자바스크립트 런타임으로 자바스크립트 프로그램을 컴퓨터에서 실행할 수 있는 환경

4. 이벤트 기반

이벤트 루프: 이벤드 발생 시 호출할 콜백 함수를 관리함(콜백 함수의 실행순서 결정). 노드가 종료될 때까지 이벤트 처리 작업 반복하므로 루프라 불림
콜백 큐(=태스크 큐): 이벤트 발생 후 호출될 콜백 함수들이 대기하는 공간. 이벤트 루트가 정한 순서에 따라 대기하므로 콜백 큐라 불림.
백그라운드: 타이머나 I/O 작업 콜백 또는 이벤트 리스너들이 대기하는 공간
이벤트 실행 원리
1.
이벤트 발생 시, 호출 스택에 함수가 쌓임.
2.
스택에 쌓인 함수가 역순으로 콜백 큐에 들어가서 실행 대기함.
3.
이 때, 비동기 처리할 함수들은 백그라운드에서 대기하다가 로직에 따라 콜백 큐로 이동함

5. 논블로킹 I/O

논블로킹: 이전 작업의 완료를 위해 멈추지 않고 다음 작업을 수행하는 것
I/O(input/output): 파일 시스템 접근(읽기, 쓰기 등)이나 네트워크 요청 등
자바스크립트는 싱글 스레드 방식의 한계 때문에 I/O 작업이 주로 논블로킹에 따른 시간적 이득을 봄

6. 싱글스레드

프로세스 vs 스레드
→ 프로세스: 운영체제에서 할당한 작업의 단위, 프로그램(브라우저는 개별적 프로세스). 프로세스 간 메모리 등의 자원 공유 X
→ 스레드: 프로세스 내에서 실행되는 흐름의 단위. 부모 프로세스의 자원 공유 O.
싱글 스레드 장단점
→ 장점: 상대적(멀티스레드와 비교)으로 컴퓨터 자원 적게 사용, 프로그램밍 생산성 높음(스레드 하나만 고려)
→ 단점: CPU 코어를 하나만 사용, 에러 처리 이상 시 서버 전체 다운 가능성
→ 결론: 스레드는 하나여도 논블로킹 방식으로 많은 수의 I/O 처리 가능하나 CPU 부하가 큰 작업에는 부적합(블로킹 발생 가능). 데이터의 개수는 많아도 크기는 작은 서비스 개발 시 적합함

7. package_lock.json

활용

1. 회원가입 기능

기본 로직
1.
db에 requested info 저장
2.
db 정상 저장 시 정상 신호 또는 에러 response (→client)
상세 로직
1.
post request 받기( 특정 경로, callback1)
2.
callback1 실행(req, res)
→ request body 정보 담고 있는 DB 객체 호출 및 instance 생성(user)
→ DB instacne에 request body 정보 저장(user.save) 후 callback2(err, userInfo) 호출
→ callback2 실행
→ user.save 작동 이상 시 error 반환(res.json 형태)
→ 정상 작동 시 성공메세지 반환(res.status(200).json 형태)
// index.js app.post("/api/users/register", (req, res) => { const user = new User(req.body); user.save((err, userInfo) => { if (err) return res.json({ success: false, err }); return res.status(200).json({ success: true, }); }); });
JavaScript
복사
Setting for Dependency: body-parser 설치(request body의 데이터를 파싱)
비밀 정보 보호(feat. .gitignore)
→ 보호해야할 코드는 특정 파일에 저장 후 .gitignore에 파일명을 입력하여 git으로 공유되는 것을 제한함
→ 환경변수 설정에 따라 db 주소 참조환경을 다르게 설정함(DB key를 가져올 때, 개발환경이 Local일 경우 ./dev에서, Production인 경우 ./prod에서)
// key.js if (process.env.NODE_ENV === "production") { module.exports = require("./prod"); } else { module.exports = require("./dev"); }
JavaScript
복사
비밀번호 암호화(feat. Bcrypt)
→ DB 저장(user.save) 전 암호화 필요(userSchema.pre)
→ 상세 로직
1.
DB 저장(user.save 호출) 과정 중 password 필드 수정 발생 시만 다음 로직 수행
→ password 수정 발생 X, next 호출(next()는 save() 지칭)
2.
salt 생성(bcrypt.genSalt())
→ 에러 처리(next(err) 반환)
→ hased password 생성(+ salt)
1.
에러 처리(next(err) 반환)
2.
DB 내 password 필드에 hased password 저장
// User.js userSchema.pre("save", function (next) { let user = this; if (user.isModified("password")) { bcrypt.genSalt(saltRounds, function (err, salt) { if (err) return next(err); bcrypt.hash(user.password, salt, function (err, hash) { if (err) return next(err); user.password = hash; next(); }); }); } else { next(); } });
JavaScript
복사

2. 로그인 기능

기본 로직
1.
requested info 중 email 필드값 DB에서 검색
2.
DB 내 일치하는 email 있다면, user 비밀번호 일치여부 확인
3.
일치하는 비밀번호 있다면, 토큰 생성
4.
토큰 정상 생성 시 쿠키에 토큰 저장
상세 로직(동기적 callback과 비동기적 callback을 유심히 볼 것)
1.
post request 받기(특정 경로, callback 1 호출)
2.
callback 1 실행(req, res)
a.
req email 정보를 DB에서 검색(req email, callback2 호출(err, user)
b.
callback 2 실행
→ 에러처리(req email이 DB 내 없으면 res.json 반환)
→ req PW와 DB 내 hashed PW 비교 함수 호출(req PW, DB PW, 비동기 callback 3 호출(err, bln))
→ PW 비교 함수 실행(req PW, DB PW, callback 4 호출(err, bln))
→ callback 4 실행(err, bln)
→ 에러처리( callback3(err) 반환)
callback3(null, bln) 반환
→ 비밀번호 불일치 시, 불일치 메세지 반환(res.json)
→ 비밀번호 일치 시 토큰 생성 함수 호출
→ 토큰 생성 함수 실행(비동기 callback 5 호출)
→ 고유값(user._id) 기반 토큰 생성(jsonwebtoken 활용)
→ DB 내 토큰 필드에 생성한 토큰 저장(callback 6 호출(err, user))
→ callback 6 실행
에러 처리( callback 5(err) 반환)
callback5(null, user) 반환
→ 에러 처리( 토큰 생성 실패 시 res.status(400).send 반환)
→ client browser 내 쿠키에 토큰 저장 후 성공 메세지 반환(res.cookie().status().json)
// index.js app.post("/api/users/login", (req, res) => { User.findOne({ email: req.body.email }, (err, user) => { if (!user) { return res.json({ loginSuccess: false, message: "해당하는 이메일이 없습니다.", }); } user.comparePassword(req.body.password, (err, isMatch) => { if (!isMatch) return res.json({ loginSueccess: false, message: "비밀번호가 틀렸습니다.", }); user.generateToken((err, user) => { if (err) return res.status(400).send(err); // 쿠키에 token 저장 return ( res .cookie("x_auth", user.token) .status(200) .json({ loginSuccess: true, userId: user._id }) ); }); }); }); });
JavaScript
복사
// User.js userSchema.methods.comparePassword = function (plainPassword, callback) { bcrypt.compare(plainPassword, this.password, function (err, isMatch) { if (err) return callback(err); callback(null, isMatch); }); }; userSchema.methods.generateToken = function (callback) { let user = this; let token = jwt.sign(user._id.toHexString(), "secretToken"); user.token = token; user.save(function (err, user) { if (err) return callback(err); callback(null, user); }); };
JavaScript
복사

3. Auth(인증) 기능

인증 기능의 필요성: client에서 페이지 이동 시, 권한 확인(일반 User or Admin) 또는 특정 기능 권한 확인(게시물 업로드 또는 삭제 등)
기본 로직
1.
client browser 내 cookie에 저장된 Token을 Server로 이동
2.
Token을 복호화하여 UserID 확인
3.
해당 UserID가 DB 내 있는 지 확인
4.
DB 내 User collection의 token filed와 cookie에서 가져온 token 비교하여 인증여부 확인
5.
인증 확인에 따른 유저 정보 갱신(?)
상세 로직
1.
get으로 requested info 받기(경로, auth 호출, callback 1 호출)
2.
auth 실행(req, res, next)
→ 클라이언트 쿠키에서 토큰 가져오기(let token = req.cookies.x_auth)
→ 토큰 복호화 함수 호출
→ 토큰 복호화 함수 실행(token, 비동기 callback 2 호출)
→ JWT의 verify 메소드 활용하여 token decode(token, "secretToken", callback 3호출)
→ callback 3 실행(err, decoded)
→ user의 필드 중 id와 token이 같은지 확인({_id: decoded, token:token}, callback 4 호출)
→ callback 4 실행
→ 에러처리(callback 2 반환)
callback2(null, user) 반환
→ callback 2 실행
→ 에러처리(callback 2 반환 값에 user가 없으면 인증 X)
req.token = token, req.user = user 이미 같은 값이 들어가 있지 않나??
→ next() 실행(middleware auth 탈출)
3.
callback 1 실행
→ auth에서 받아온 token와 user info 활용하여 response
→ isAuth: true, isAdmin: req.user.role 등
// auth.js const { User } = require("../models/User"); let auth = (req, res, next) => { let token = req.cookies.x_auth; User.findByToken(token, (err, user) => { console.log(user); if (err) throw err; if (!user) return res.json({ isAuth: false, error: true }); req.token = token; req.user = user; next(); }); }; module.exports = { auth };
JavaScript
복사
// User.js userSchema.statics.findByToken = function (token, callback) { let user = this; jwt.verify(token, "secretToken", function (err, decoded) { user.findOne({ _id: decoded, token: token }, function (err, user) { if (err) return callback(err); callback(null, user); }); }); };
JavaScript
복사
// index.js app.get("/api/users/auth", auth, (req, res) => { res.status(200).json({ _id: req.user._id, isAdmin: req.user.role === 0 ? false : true, isAuth: true, email: req.user.email, name: req.user.name, lastname: req.user.lastname, role: req.user.role, image: req.user.image, }); });
JavaScript
복사

4. 로그아웃 기능

기본 로직
1.
로그아웃 해당 유저를 DB에서 검색
2.
DB 내 해당 유저 filed 내 token 삭제
상세 로직
1.
get으로 requested info 받기(경로, auth, callback 1 호출)
2.
callback 1 실행
→ findOneAndUpdate 함수 실행( 찾으려는 정보의 필드(id), 바꿀 정보의 필드(token), callback 2 호출)
→ callback 2 실행
→ 에러처리( 로그아웃 실패 반환 res.json)
→ 성공메세지 반환(res.status().send)