컴퓨터/웹 : JS

[Node.js](project)로그인 구현/passport/session

도도새 도 2023. 2. 22. 10:43

로그인 구현

 

오늘은 이제 로그인 구현을 할 차례이다. 이를 위해 스키마를 다시 들여다봤다. 몽구스 스키마를 수정하던 중 카페 스키마에 rating을 넣었는데, 이를 동적으로 바꾸고자 했다. user가 남긴 comment내의 rating점수의 평균을 자동으로 avgRating이라는 이름의 필드에 계산해주면 참 편하겠구나 했다.

 

이 방법 중 하나로 버츄얼 속성이라는 것이 있다. 이 가상 속성이라는 것은 필드에 할 작업(?)을 미리 정의해 놓을 수 있다. 예를 들어 a라는 필드 값이 b2배가 되게 하라! 하면 모델이 생성될 때 그렇게 된다.

 

그래서 약간 조사 후 알게 된 것이 가상 속성의 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. 사용해준다.

 

...작성 하고 나서 기억한 것이 usernamepassword는 후에 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정보 등을 받아와야한다.

 

재밌겠으면서도 피곤하다.