[Node.js](project)로그인 구현/passport/session
로그인 구현
오늘은 이제 로그인 구현을 할 차례이다. 이를 위해 스키마를 다시 들여다봤다. 몽구스 스키마를 수정하던 중 카페 스키마에 rating을 넣었는데, 이를 동적으로 바꾸고자 했다. 즉 user가 남긴 comment내의 rating점수의 평균을 자동으로 avgRating이라는 이름의 필드에 계산해주면 참 편하겠구나 했다.
이 방법 중 하나로 버츄얼 속성이라는 것이 있다. 이 가상 속성이라는 것은 필드에 할 작업(?)을 미리 정의해 놓을 수 있다. 예를 들어 a라는 필드 값이 b의 2배가 되게 하라! 하면 모델이 생성될 때 그렇게 된다.
그래서 약간 조사 후 알게 된 것이 가상 속성의 get함수가 호출되는 경우에만 필드에 내가 정의한 속성이 적용된다. 따라서 내가 특정 필드를 업데이트하는 것만으로 다른 필드가 영향을 받는 것은 버츄얼 속성으로 할 수가 없다는 것이다.(하긴 이렇게 된다면 이게 다 자원낭비일 것이다) 대신 아래와 같은 방법은 있다.
cafeSchema.virtual('a').get(function() {//버츄얼 예시: a에 대한 속성 정의
return this.b * 2; //a속성은 이것의 b속성 값의 2배가 된다!(this는 보통 이 메서드를 호출한 객체를 즉, 자기자신을 가르킨다.)
});
const cafe =Cafe.findbyid(아이디값)
cafe.b = 10;
cafe.a = cafe.get(‘b’);//get함수를 직접 사용
그럼 a값이 get에서 정의한 대로 적용된다. 난 우선 아래와 같이 넣어두었다.
cafeSchema .virtual ('ratingAVG').get (()=> {
const comments = this .comment || [];
const numComments = comments .length ;
const ratings = comments .map (comment =>comment .rating || { taste :0 , atmosphere :0 , price :0 });
const sum = {
taste :ratings .reduce ((total , rating ) =>total + rating .taste , 0 ),//total = 누적값, rating = 배열의 현재요소
atmosphere :ratings .reduce ((total , rating ) =>total + rating .atmosphere , 0 ),
price :ratings .reduce ((total , rating ) =>total + rating .price , 0 )
};
return {
taste :numComments ? sum .taste / numComments : 0 ,
atmosphere :numComments ? sum .atmosphere / numComments : 0 ,
price :numComments ? sum .price / numComments : 0
};
});
.map은 배열을 순회하여 콜백함수로 처리한 결과를 새 배열로 할당한다.
.reduce는 배열을 순회하며 콜백함수로 처리한 결과를 누적한다(더한다)
일단 그리고 유저 스키마를 작성해보자.
const mongoose = require ('mongoose');
const cafe = require ('./cafe');
const Schema = mongoose .Schema ;
const userSchema = new Schema ({
userName :{ //사용X
type :String ,
required :true ,
unique :true
},
password :{//사용X
type :String ,
require :true
},
email :{
type :String ,
required :true ,
unique :true
},
reviewedCafe :[{
type :Schema .Types .ObjectId ,
ref :'Cafe'
}],
comments : [{
type :Schema .Types .ObjectId ,
ref :'Comment'
}],
likes :[{
type :Schema .Types .ObjectId ,
ref :'Cafe'
}],
isBanned : {
type :Boolean ,
default :false
},
warnings : {
type :Number ,
default :0
}
});
const User = mongoose .model ('User', userSchema );
module .exports = User ;
아이디 이메일 비밀번호를 당연히 가지고, 경고와 벤 여부까지 가져야 한다. 대충 위처럼 작성하면 잘 작동하지 않을까... 그리고 비밀번호는 save하기 전에 해쉬함수를 통과시켜서 암호화시켜야한다. 해쉬를 통과하면 비밀번호가 랜던하게 암호화가 되는데, 누가 DB를 들여다보더라도 비밀번호를 알 수 없게 된다.
예를 들어 이 작업을 하지 않으면 내가 내 사이트를 이용하는 사람들의 아이디 비밀번호를 쉽게 탈취할 수 있을 것이다.
이 모든 작업을 쉽게 해주는 모듈이 있다. passport-local-mongoose. 사용해준다.
...작성 하고 나서 기억한 것이 username과 password는 후에 passport에 의해 자동 생성이 될 것이다. 여기서 생성하면 오류가 발생! 유의! 코드에는 후에 또 passport를 이용할 때 주의하라고 여전히 기록을 해놓았다.
로그인 페이지 구현
로그인 페이지와 회원가입 페이지까지 대충 만들어준다.
로그인에 글자가 가입하기로 되어있는 건 수정했따...
passport와 관련된 session등을 인스톨하고 코드 몇 줄을 적는다.
const passport = require ('passport');
const LocalStrategy = require ('passport-local').Strategy ;
app .use (passport .initialize ());
app .use (passport .session ());
app .use (express .urlencoded ({ extended :true }));
passport .use (new LocalStrategy (User .authenticate ()));
passport .serializeUser (User .serializeUser ());//사용자 정보 세션에 저장
passport .deserializeUser (User .deserializeUser ());//세선의 값을 HTTP Ruest 리턴(req.user로 확인가능)
app .post ('user/signup', (req , res ) => {
const newUser = new User ({username :req .body .username });
User .register (newUser , req .body .password , (err , user ) => {
if (err ) {
console .log (err );
return res .render ('signup');
}
passport .authenticate ('local')(req , res , () => {
res .redirect ('/');
});
});
});
고작 저 몇 줄로 회원가입은 완료되었다. User.register()이 하나의 코드로 무려 회원가입이 된 것이다! 잘 생성되었나 몽고쉘을 켜서 확인해본다.
에러
MongoServerError: E11000 duplicate key error collection: Cafe.users index: userName_1 dup key: { userName: null }
오류가 발생한다.... 그것도 계속. users 컬렉션은 drop하니 해결되었다.
그리고 또 에러가 거듭 발생했는데, 내가 회원가입 구현을 할 때 필수 정보들을 넣지 않았다는 것이다. 즉 내가 몽고 스키마를 보면 필드 값을 required로 해놓은 것들이 있는데(이메일 등) 내가 그것들을 데이터 베이스에 저장하지 않아 저장 자체가 되지 않았다.
게다가 나는 name = A[password]라는 방식으로 html에서 데이터를 넘겼는데, 이러면 password데이터는 req.body.A.password로 저장이 되어있다. 나는 위 코드에서는 빈 객체를 저장했던 것이다.
app .post ('/user/signup', async (req , res ) => {
try {
inputedUser = req .body .user ;
const user = new User ({
username :inputedUser .username ,
email :inputedUser .email ,
nickName :inputedUser .nickName
});
const registerUser = await User .register (user , inputedUser .password );
req .login (registerUser , err =>{
if (err ) return next (err );
res .redirect ('/cafe');
});
}catch (e ){
console .log (e );
}
});
결국 이렇게해서 회원가입을 구현하였다.
이게 참 로그인은 모듈을 사용해서 하라는데로 구현하긴 하는데, 내부적으로 상세히 어떻게 되는지를 잘 모르겠다.
대충 해야하는 동작은 아이디 중복 검사하고 해쉬함수를 돌린 비밀번호를 저장하고 세션에 해당 아이디로 로그인 했다는 것을 알리는 것. 그리고 로그아웃시 세션을 파괴하는 것. 또한 로그인시 또 인풋으로 들어오는 아이디와 해쉬 함수를 돌린 비밀번호 조합이 기존 db의 아이디 비밀번호와 일치하는가 확인하기. 이 동작들이다. 이걸 passport에서 한번에 해주기는 한다. 그런데 공식 문서를 봐도 패스포트가 정확히 어떻게 작동하는지 한 눈에 보기는 참 힘들다. 언제 한번 날 잡고 문서를 다 독해하고 시도해봐야겠다. 그러다보면 도큐먼트를 보는 피로감이 덜해지지 않을까.
로그인도 간단하게 구현한다.
app .post ('/user/signin', passport .authenticate ('local', {falureFlash :true , failureRedirect :'/login'}), async (req , res )=>{
res .redirect ('/cafe');
})
app .get ('/user/logout', async (req , res , next )=>{
req .logout ((err )=>{
console .log (err );
return next (err );
})
res .redirect ('/cafe');
})
아무튼 로그인과 로그아웃까지 구현 완료. 이제 폼 자체에 validation을 더하고, 이메일 인증 ....등을 더하면 좋겠지만 이건 조금 더 생각해보기로 하자. 우선은 메인 기능에 충실하기! 유저의 _id가 없어서 하지 못햇던 댓글-유저 연동을 진행해야한다. 즉 로그인 된 상태여야만 댓글을 달 수 있도록 처리하기.
추가적으로 로그인상태가 되면 버튼이 logout으로 바뀌고 반대 상황이면 login되도록 네비게이션바 ejs코드랄 살짝 수정하였다.
아무튼 이제 할 건 공포의 리액트 파트다.
여기서 마주한 문제는 아래와같다.
1. ejs파일들에서 공통적으로 쓰는 boilerplate코드를 ejs와 같은 방식으로 받아올 수 없다.(ejs는 서버측 엔진이다) 컴포넌트로 변환해서 붙여야한다.
2. 세션은 일반적으로 서버사이드에 저장된다. 그런데 리액트는 클라이언트 사이드이다. 즉, 내가 로그인을 하여 세션이 남아있더라도 그 세션을 클라이언트에서는 쓸 수가 없다. 따라서 리액트 페이지가 실행 될 때마다 axios로 서버측에 현재 사용자가 로그인 중인지 혹은 db정보 등을 받아와야한다.
재밌겠으면서도 피곤하다.