Project/[SAFU] 1st Project

5. [Client & Server] Login, Logout 구현 (Oct 21, 2020 ~ Oct 25, 2020 회고)

HJChung 2020. 11. 5. 18:07

10/17~10/20까지 Signup 컴포넌트 구현과 Server API를 코드리뷰 하면서 일단 1차로 마무리를 시켰다. 
여기서 회원가입이 완료되면 Login 컴포넌트로 Redirect가 되어야하는데 아직 Login 컴포넌트가 완성이 되지 않아서 

이를 새로운 이슈카드로 빼놓고 일단 PR & upstream-client로 merge 까지 해 놓은 상태였다. 

 

10/21~ 부터 Login/Logout 컴포넌트 기능 구현 작업에 들어갔다. 

<Login 컴포넌트>의 해야 할 것은 

  1.  login 컴포넌트 틀 잡기
  2.  일반 로그인 구현
  3. log in 버튼 누르면 -> redirect -> (logged) main 페이지
  4. 회원이 아니신가요? Sign up 버튼 누르면 -> redirect -> Sign up 페이지
  5. findId 버튼 누르면 -> redirect -> find Id 페이지
  6.  findPW 버튼 누르면 -> redirect -> find PW 페이지

 

 

 

 

1.  login 컴포넌트 틀 잡기 - (Oct 21, 2020) 회고

컴포넌트 틀을 잡는 것은 어려운 것이 아니었는데 session부분에서 확실히 이해를 하지 못하는 것같은 찝찝함이 들어서 express-session middleware사용에 대해서 복습하였다. ↓

express-session middleware 사용

1. 설치

terminal)

npm install express-session

2. 사용 

index.js) express에 express-session 적용

var session = require('express-session')
var express = require('express')
var parseurl = require('parseurl')
var session = require('express-session')
 
var app = express()
 
app.use(session({ //사용자의 요청이 있을 때마다 session()함수를 실행시킨다. 그러면 session이 시작된다. (즉 우리 서비스가 session을 사용할 수 있게 된다.)
  secret: 'safuProject',
  resave: false,
  saveUninitialized: true
})) //이 객체 안에 있는 옵션에 따라 session이 동작하는 기본 방법이 달라진다.   
 
app.get('/', function(req, res, next){
	console.log(req.session); //이렇게 하면 생성된 session이 출력된다. 
	res.send('Hello session!');
}

app.listen(3000, functionn(){
	console.log('3000!')
 }

  

express-session의 옵션)

1. secret: Required option. 노출되면 안되는 정보. 

쿠키를 임의로 변조하는것을 방지하기 위한 값 입니다. 이 값을 통하여 세션을 암호화 하여 저장

2. resave: false 권장. session 데이터가 바뀌기 전까지는 session 저장소의 값을 저장하지 않는다. 

true면 session데이터의 변경 유무에 상관없이 무조건 session 저장소에 저장한다. 

3. saveUninitialized: 세션이 저장되기 전에 uninitialized 상태로 미리 만들어서 저장할지에 대한 것. true 권장. true면 session이 필요하기 전까지는 session을 구동시키지 않는다. 

 

3. session에 접근 

req.session을 통해 세션에 접근할 수 있다. 

app.get('/', function(req, res, next){
	console.log(req.session); //이렇게 하면 생성된 session이 출력된다. 
	res.send('Hello session!');
}

위와 같이 req.session을 출력하면 이렇게 session이 출력된다.

Session {
  cookie:
   { path: '/',
     _expires: null,
     originalMaxAge: null,
     httpOnly: true }}

session middleware가 request에 session 객체를 추가해주기 때문이다. 

 

4. session 생성

 session 객체 내에 우리가 원하는 속성을 추가(생성) 할 수도 있다. 

//세션 생성
req.session.세션명 = 세션value;
app.get('/', function(req, res, next){
	req.session.userid = data.dataValues.id; //이렇게 session객체에 userid라는 key와 data.dataValues.id 값을 value로 하는 속성을 추가. 
	console.log(req.session); 
	res.send('Hello session!');
}

그러면  이렇게 session과 출력된다. 

Session {
  cookie:
   { path: '/',
     _expires: null,
     originalMaxAge: null,
     httpOnly: true },
  userid: 12 }

5. session 저장

사용자의 session 데이터는 어디에 저장할까?

휘발되지 않는 곳에 저장하면 좋을 것이다. 이러한 세션 데이터의 저장소를 세션 저장소라고 한다. 

var express = require('express')
var parseurl = require('parseurl')
var session = require('express-session')
var FileStore = require('session-file-store')(session)//이거 추가!
 
var app = express()
 
app.use(session({
    secret: 'gracestudyblog',
    resave: false,
    saveUninitialized: true,
    store:new FileStore() //이거 추가!
}))
 
app.get('/', function(req, res, next){
	req.session.userid = data.dataValues.id;
	console.log(req.session); 
	res.send('Hello session!');
}
 
app.listen(3000, function () {
    console.log('3000!');
});

이렇게 하면 파일에 session 정보가 기록된다. 

2. 일반 로그인 구현 - (Oct 21, 2020) 회고

위에서 복습한 것을 참고하고 POST login API를 사용해서 로그인 기능(로그인 후 session의 생성 : req.session.세션명=세션값)을 구현하였다.

client) 로그인에 필요한 email, password정보가 state에 담겨있고 이를 server로 보내주는 post요청 

handleLoginButton = () => {
    axios({
      method: 'post',
      url: 'http://localhost:4000/users/login',
      data: {
        useremail: this.state.useremail,
        password: this.state.password,
      },
    })
      .then((res) => {
        //status 가 200이면,
        this.setState({ isLoginMessage: true });
      })
      .catch((err) => {
        //status가 401이면
        if (err.message === 'Request failed with status code 401') {
          this.setState({ isLoginMessage: false });
        }
        //그게 아니면 서버에러
      });
  };

server)

- Server 파트는 HI님께서 구현해주신 것이고 코드리뷰를 했다. 

index.js 여기서 express-session 미들웨어를 사용한다고 선언해주고, 

var session = require('express-session')
const usersRouter = require('./routes/users');
 
var app = express()
 
app.use(session({
    secret: 'gracestudyblog',
    resave: false,
    saveUninitialized: true,
}))
 
app.use('/users', usersRouter);

라우팅을 거쳐서

routes/reviews.js

const express = require('express');
const router = express.Router();

const { usersController } = require('../controller');

//POST login
router.post('/login', usersController.login.post);
//GET logout
router.post('/logout', usersController.logout.get);

module.exports = router;

실제 콜백함수로 실행되는 것이 controller에 구현되어 있다.  여기서 로그인 성공시 session 생성

controller/users/login.js

const { users } = require('../../models');

module.exports = {
  post: (req, res) => {
    const { useremail, password } = req.body;
    users
      .findOne({
        where: {
          email: useremail,
          password: password,
        },
      })
      .then((data) => {
        if (!data) {
          res.status(401).send('unvalid user');
        } else {
          req.session.userid = data.dataValues.id; //여기서 session객체에 userid라는 key와 data.dataValues.id 값을 value로 하는 속성을 추가. 
          res.status(200).json({ id: data.dataValues.id });
        }
      })
      .catch((error) => {
        res.sendStatus(500);
      });
  },
};

그러면 로그인 성공시 서버가 response의 header로 set-cookie: connect.sid = ~~~~ 이런 것이 들어가 있는 것을 확인 할 수 있다.

connect.sid = ~~~~ 는 session의 식별자(session id)로, 쿠키의 형식으로 웹 브라우저에 박혀있는 것이다. 

 

그러면 flow가,

 

 

 

[Client] 로그인을 위한 post 요청을 보냄.

[Server] 세션을 생성

[Client] 세션을 식별할 수 있는 id와 값이(connect.sid = ~~~~ )가 쿠키형태로 Client에 저장

[Client] session이 유효한 이상 Server에 요청할 때 이 id를 같이 전송(request headers에 있음)

[Server] 이 id로 각 Client를 식별하고, 해당 Client의 세션을 사용.

 

 

+ 우리 서비스에 사용하지는 않았지만 추가 공부한 것. 은 <더보기> 클릭

더보기

2) 로그인 후 session 생성되고  session 저장 : req.session.save()

위에서 살펴본 <5. session 저장>에서 살펴본 세션 저장소를 사용한다고 할 때, 어떻게 구현할 수 있을 까?

server)

앞의 과정은 login과 동일하고,

실제 콜백함수로 실행되는 것이 controller에 구현되어 있다. -- 여기서 로그인 성공시 session 생성 및 저장

controller/users/logout.js

const { users } = require('../../models');

module.exports = {
  post: (req, res) => {
    const { useremail, password } = req.body;
    users
      .findOne({
        where: {
          email: useremail,
          password: password,
        },
      })
      .then((data) => {
        if (!data) {
          res.status(401).send('unvalid user');
        } else {
          req.session.userid = data.dataValues.id; //여기서 session 생성
          req.session.save(function(){//req.session.savve 메소드: session 저장 메소드
          	//session 저장이 완료되면 실행되는 이 콜백 함수에서 원하는 동작을 시켜줄 수 있다. 예를 들어 Mypage로 redirect하길 원하면,
          	res.redirect('/mypage');
          })
          res.status(200).json({ id: data.dataValues.id });
        }
      })
      .catch((error) => {
        res.sendStatus(500);
      });
  },
};

 

 

 

3. 로그인 후 Redirect 구현 (log in 버튼 누르면 -> redirect -> (logged) main 페이지) -  (Oct 24, 2020) 회고

로그인이 완료되면 로그인 된 main페이지로 redirect가 된다. 

처음에는 POST login API응 응답이 정상으로 오면 this.props.history.push('/') 를 쓰려고 했다.  

 

그러나 우리 서비스의 flow는 

[Client] main 페이지에 딱 들어가자마자 GET review를 요청

[Server] session을 확인하여서 session이 있으면 {isLogin:true}라는 값을 응답 json에  추가로 준다. 
[Client] 응답 json에 에서 이에 따라 isLogin state를 변경하고, 로그인 전 상태(isLogin:false)에서 로그인 후 상태(isLogin:true)로 화면 구성이 바뀐다. 는 로직을 가지고 있었기 때문에 

 

그냥 this.props.history.push('/')을 쓰면 Main 페이지로 보내지는 대신 그냥 위치만 이동한 것이므로 재랜더링이 되지 않는다. 즉, isLogin 상태 false에서 true로 바뀔 겨를이 없는 것이다. 

 

그래서 어떻게 하면 redirect가 되어서 재랜더링 되고 다시 GET review를 요청할 수 있을까 생각하던 중

HJ님께서 window.location='/'을 사용하며 잘 된다고 가르쳐 주셨다!

handleLoginButton = () => {
    axios({
      method: 'post',
      url: 'http://localhost:4000/users/login',
      data: {
        useremail: this.state.useremail,
        password: this.state.password,
      },
    })
      .then((res) => {
        this.setState({ isLoginMessage: true });
      })
      .then(() => {
        window.location = '/'; //log in 버튼 누르면 -> redirect -> (logged) main 페이지
      })
      .catch((err) => {
        if (err.message === 'Request failed with status code 401') {
          this.setState({ isLoginMessage: false });
        }
        //그게 아니면 서버에러
      });
  };

※ Javascript의 window.location

 window.location개체는 현재 페이지 주소 (URL)를 가져오고 브라우저를 새 페이지로 리디렉션하는 데 사용할 수 있다.

① 새 페이지로 이동하기

location.assign("http://www.mozilla.org"); // 또는
location = "http://www.mozilla.org";

② 서버로부터 현재 페이지 강제로 다시 로드하기

location.reload(true);

 

여기까지 구현하니까

  1. 회원이 아니신가요? Sign up 버튼 누르면 -> redirect -> Sign up 페이지
  2. findId 버튼 누르면 -> redirect -> find Id 페이지
  3. findPW 버튼 누르면 -> redirect -> find PW 페이지
    와 같은 리다이렉트 만 남아 있었다. 

여기서 또 아직 Findid, Findpw 컴포넌트가 완성이 되지 않아서 버퍼링이 걸렸다. 

그래서 잠시 멈추고 Findid, Findpw 컴포넌트를 만들러갔다.

 

※ 이 과정에서 배운 것: 

1) 이슈카드는 큰 덩어리로 분류하고, 각 덩어리 안에서는 기능 flow를 고려하여서 세분화 및 순서 부여를 시키는게 좋다. 

저번주만 하더라도 Client 쪽 issue card는 컴포넌트(솔직히 말하면 우린 SPA인데도 불구하고 페이지 기준)만을 기준으로 되어있었다. 그런데 리다이렉트가 빈번한 서비스이다보니 컴포넌트 간 구현 순서 역시 중요했다. 그래서 해당하는 것들을 더욱 세분화 하여서 순서대로 issue카드를 수정하게 되었다. 그래서, 

이렇게 하나로 퉁쳤던 issue 카드를

login 기능 구현 으로 퉁쳤던 걸 

-----------------------------------→

 

이렇게 세분화!

 

 

 

 

 

4. 로그인 관련 Redirect 구현 -  (Oct 25, 2020) 회고

Findid, Findpw 컴포넌트 구현을 일정 부분까지 해 놓은 뒤 위에서 언급한 것들을 했다. 

우선 버튼을 눌러서 redirect 될 수 있게 이벤트를 부여했다. 

          <ul>
            <button
              onClick={(e) => {
                e.preventDefault();
                this.props.history.push('/Findid'); 누르면 Findid 컴포넌트로 가는 Find Id 버튼 
              }}
            >
              Find Id
            </button>
            <button
              onClick={(e) => {
                e.preventDefault();
                this.props.history.push('/Findpw');누르면 Findpw 컴포넌트로 가는 Find Id 버튼 
              }}
            >
              Find PW
            </button>
          </ul>
        </div>
        <div>
          {this.state.isLoginMessage === false ? ( 
            <div>
              <span> 회원이 아니신가요?</span>
              <button>Sign up</button>
              <button
                onClick={(e) => {
                  e.preventDefault();
                  this.props.history.push('/SignUp'); 누르면 Signup 컴포넌트로 가는 Find Id 버튼 
                }}
              >
                Sign up
              </button>
            </div>

그런데 여기까지만 하니까 주소창은 /Findid로 찍히는데 계속 /인 Main페이지로 리다이렉트 되는 것이다! 한참을 고민하다가 결국 알아냈다. 내가 /Findid, /Findpw 라우팅 주소를 <Findid/>, <Findpw/> 컴포넌트와 연결시켜 놓지 않았던 것이다. 

그래서 우리 서비스의 총 라우팅을 담당하는 Nav.js에서

<Switch>
     <Route path="/" exact component={Main}></Route>
     <Route path="/SignUp" component={SignUp}></Route>
     <Route path="/Login" component={Login}></Route>
     <Route path="/Findid" component={Findid}></Route>
     <Route path="/Findpw" component={Findpw}></Route>
</Switch>

이렇게  전부 라우팅을 해주었더니 그제서야 잘 작동된다!

 

※ exact component???

exact는 주어진 경로와 정확히 맞아 떨어져야만 설정한 컴포넌트를 보여준다. exact을 사용하지 않으면 만약 /SignUp으로 들어올 경우, /와 /SignUp이 모두 매칭 되기 때문에 Main 컴포넌트와 Signup컴포넌트 두개가 보이게 된다. exact를 사용하면, 매칭이 될 경우 하위 라우트 설정을 보지 않게된다.

 

 

그 외에도 React의 Routing 사용방법이 많은 것 같다. 

 

velopert.com/3417

 

react-router :: 1장. 리액트 라우터 사용해보기 | VELOPERT.LOG

이 튜토리얼은 3개의 포스트로 나뉘어진 이어지는 강좌입니다. 목차를 확인하시려면 여기를 참고하세요. SPA 란? Single Page Application (싱글 페이지 어플리케이션) 의 약자입니다. 말 그대로, 페이지

velopert.com

특히 withRouter를 쓰는 방법은 더 공부를 해야겠다는 생각이 들었다.

 

5. 로그아웃 후 session의 삭제 : req.session.destroy()  -- (Oct 24, 2020) 회고

client우리 서비스에서 라우팅의 분기를 담당하는 곳을 <Nav/> 컴포넌트로 정했다. 그래서 위에서 보았듯이 Nav.js에서 Route가 다 모여있다. 그러나 로그아웃 기능은 새로운 컴포넌트가 랜더링 되는 것이 아니라 그냥 Nav 컴포넌트의 logout 버튼에 있는 이벤트이므로 바로 이벤트를 달아주었다. 

 <button
	onClick={(e) => {
		 e.preventDefault();
		{
			  this.handleLogoutButton();
		 }
	}}
>
log out
 </button>

이 버튼을 누르면 logout API에게 POST 요청을 보낸다. 

handleLogoutButton = () => {
    axios({
      method: 'post',
      url: 'http://localhost:4000/users/logout',
    })
      .then((res) => {
        console.log('로그아웃 완료');
      })
      .catch((err) => {
        //500(err)
        console.error(err);
      });
  };

 

server)

여기서 로그아웃 성공시 session 삭제

controller/users/logout.js

 app.post('/logout', function(req, res){
        if(req.session.userid){
            req.session.destroy(function(err){ //req.session.destroy 메소드: session 삭제 메소드
                if(err){
                    console.log(err);
                }else{
                    res.redirect('/'); // logout 해서 session 삭제완료 되면 에러가 나지 않았을 경우 main 페이지로 redirect
                }
            })
        }else{
            res.redirect('/');
        }
    })

 

 

 

여기까지 진행하니까,

로그인 전 main-> 회원가입--> 로그인 -> 로그인 후 main -> 로그아웃 -- alert 확인 -> 로그인 전 main

까지 매끄럽게 작동한다.

 

다음에 구현할 것은 Findid와 Findpw 이다!

 

reference

velopert.com/406