Cookie를 사용하여 로그인 상태를 저장하자

Kim-DaHam·2023년 5월 3일
0

Server

목록 보기
1/10
post-thumbnail

🔥 학습목표

  • Cookie를 사용하여 사용자의 로그인 상태를 저장한다.



🟩 Server

🟣 Express 서버 구축하기

⬜ 필요한 모듈

// express 모듈 불러오기
const express = require('express');
// cors 모듈 불러오기
const cors = require('cors');
// morgan: 로그를 남겨주는 미들웨어. 클라이언트에서 요청한 메서드나 상태 코드 등이 출력된다.
// 로그를 어떤 방식으로 찍을 것인지에 대한 옵션을 파라미터로 넣는다.
const logger = require('morgan');
// 요청된 쿠키를 쉽게 추출할 수 있도록 도와주는 미들웨어
const cookieParser = require('cookie-parser');
// node.js에서 파일 입출력 처리를 할 때 사용하는 모듈
const fs = require('fs');
// https 프로토콜을 사용하기 위해 node.js에 내장된 모듈
const https = require('https');
// 메서드 및 요청 경로 별 수행할 동작 모음
const controllers = require('./controllers');
// 서버 생성
const app = express();

🎁 cookie-parser
🎁 morgan logger
🎁 https 설정하기


⬜ 미들웨어 등록

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

🔵 express.json()와 express.urlencoded()

: 클라이언트로 부터 받은 http 요청 메시지 형식에서 body 데이터를 해석하기 위해 필요한 처리.

(위와 같은 처리 없이 클라이언트에서 입력한 form 값을 출력하면 undefined 라고 나온다.)

  • express.json() : json 형태의 데이터를 해석해준다.

  • express.urlencoded() : x-www-form-urlencoded형태의 데이터를 해석해준다.

(form 으로 제출된 응답 reqheaders 출력 후 content-type 를 확인하면 데이터 타입을 알 수 있다.)

🎁 자세한 참고


⬜ CORS 설정하기

app.use(
  cors({
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST', 'OPTIONS'],
    credentials: true,
  })
);

http://localhost:3000 출처와 리소스를 공유하며, ['GET', 'POST', 'OPTIONS'] 요청만 허가한다.

다른 도메인에 요청을 보낼 때 요청에 인증(credential) 정보를 담아서 보낼 지를 결정하는 항목인 credentialstrue 로 설정한다.


⬜ https 서버를 위해 mkcert에서 발급한 인증서 사용

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

const HTTPS_PORT = process.env.HTTPS_PORT || 4000;

🎁 Node.js에서 환경변수


⬜ http/https 서버 실행

인증서 파일이 존재하는 경우 https 서버를 실행하고, 존재하지 않는 경우 http 서버를 실행한다.

let server;
if (fs.existsSync('./key.pem') && fs.existsSync('./cert.pem')) {
  const privateKey = fs.readFileSync(__dirname + '/key.pem', 'utf8');
  const certificate = fs.readFileSync(__dirname + '/cert.pem', 'utf8');
  const credentials = {
    key: privateKey,
    cert: certificate,
  };

  server = https.createServer(credentials, app);
  server.listen(HTTPS_PORT, () => console.log(`🚀 HTTPS Server is starting on ${HTTPS_PORT}`));
} else {
  server = app.listen(HTTPS_PORT, () => console.log(`🚀 HTTP Server is starting on ${HTTPS_PORT}`));
}
module.exports = server;

⬜ 요청 메서드 라우터

app.post('/login', controllers.login);
app.post('/logout', controllers.logout);
app.get('/userinfo', controllers.userInfo);



🟣 Controller

폴더를 이렇게 정리해놓고 index.js 파일에다

module.exports = {
  login: require('./users/login'),
  logout: require('./users/logout'),
  userInfo: require('./users/userInfo'),
};

한꺼번에 모듈을 배포하면 가독성이 훨씬 좋다는 걸 알게되었다.

앞으로 할 프로젝트나 지금까지 해온 것들 이렇게 리팩토링 해야지


⬜ 로그인 POST 요청

필요한 상태 관리

const { userId, password } = req.body.loginInfo;
  const { checkedKeepLogin } = req.body;
  • 클라이언트에서 POST 요청 시, loginInfo 객체에 유저 아이디와 비밀번호가 request body로 담겨 온다.

  • 로그인 유지 옵션을 체크했으면 true, 아니면 false 값이 넘어온다.


사용자 계정 데이터

const userInfo = {
    ...USER_DATA.filter((user) => user.userId === userId && user.password === password)[0],
  };

저장 된 회원 데이터 중 아이디와 비밀번호가 일치한 회원 정보를 불러온다.


쿠키 옵션

const cookieOptions = {
    domain: 'localhost',
    path: '/',
    sameSite: 'strict',
    secure: true,
    expires: new Date(Date.now() + 24 * 3600 * 1000 * 7), // 7일 후 소멸되는 Persistent Cookie
    httpOnly: true,
  };
  • domain & path : 쿠키를 전달 할 경로

  • secure : 현재는 localhost라 상관 없지만 습관적으로 true로 해두자.

  • httpOnly : 쿠키가 탈취 되지 않도록 무조건 true로 해두자.

  • sameSite : sameSite 내에서만 쿠키를 주고받을 수 있도록 strict로 설정

    • 서버는 http://localhost:4000, 클라이언트는 http://localhost:3000을 사용
    • 두 주소는 TLD(.com, .org ...)를 갖고있지 않아 eTLD+1가 없다.
    • 다만 동일한 host(localhost)를 갖기 때문에 sameSite로 판단한다.

주요 기능

if (!userInfo.id) {
    res.status(401).send('Not Authorized');
  } else if (checkedKeepLogin) {
    res.cookie('cookieId', userInfo.id, cookieOptions);
    res.redirect('/userinfo');
  } else {
    delete cookieOptions.expires;
	// Expires 옵션이 없는 Session Cookie
    res.cookie('cookieId', userInfo.id, cookieOptions);
    res.redirect('/userinfo');
  }
  • res.cookie : 쿠키를 전달한다. 전달인자로 쿠키 이름, 쿠키 값, 쿠키 옵션을 받는다.

  • 해당되는 유저가 없으면 401 에러 보내기.

  • 로그인 유지 체크 되었으면 유저 로그인 인증 정보로 사용할 userInfo.id를 유효기간이 7일인 쿠키로 전송한다.

  • 로그인 유지를 하지 않는 사용자라면 유효기간이 없는(브라우저 종료 시 삭제되는) 쿠키를 전송한다.

  • 로그인한 사용자 정보를 불러오기 위해 /userinfo 경로로 리다이렉트 한다. 그렇지 않으면 정보를 불러오지 않고 화면을 바꾸지 않아 계속 로그인 화면 상태로 멈춰있다.

/login 에서는 쿠키만 발급하고, /userinfo 에서는 회원 정보를 보내주는 게 포인트다!


⬜ 유저 정보 GET 요청

유저 정보(비밀번호 제외) 전송하기

(req, res) => {
  const cookieId = req.cookies.cookieId;
  const userInfo = { ...USER_DATA.filter((user) => user.id === cookieId)[0] };
  if (!cookieId || !userInfo.id) {
    res.status(401).send('Not Authorized');
  } else {
    delete userInfo.password;
    res.json(userInfo);
  }
}
  • 로그인 인증 된 사용자 id가 저장된 쿠키인 cookieId가 회원 목록에 존재하지 않는다면 에러 메세지를 보낸다.

  • 정상적으로 존재한다면 비밀번호를 제외한 회원 정보를 요청에 대한 응답으로 보내준다.


⬜ 로그아웃 POST 요청

쿠키 삭제

(req, res) => {
  res
    .status(205)
    .clearCookie('cookieId', {
      domain: 'localhost',
      path: '/',
      sameSite: 'strict',
      secure: true,
    })
    .send('Logged Out Successfully');
}
  • 205 Reset Content - form의 내용을 지우거나 캔버스 상태를 재설정하거나 UI를 새로 고치려면 client의 문서뷰를 새로고침하라고 알려준다.



🟩 Client

🟣 로그인

⬜ ID/PWD 입력하기

const [loginInfo, setLoginInfo] = useState({
    userId: '',
    password: '',
  });

const handleInputValue = (key) => (e) => {
    setLoginInfo({ ...loginInfo, [key]: e.target.value });
  };

ID, PWD를 입력받을 때 저런 식으로 상태를 덮어씌울 쑤 있는 지 몰랐다. 늘 ID input 태그의 onChange와 PWD input의 onChange를 각각 적었던 것 같은데...

그냥 키를 받아서 구조분해할당으로 덮어씌면 된다는 걸 깨달았다.

종종 이런 부분에서 머리가 왜 안 돌아갔나 싶다


⬜ ID/PWD POST 요청

axios
  .post('http://localhost:4000/login', { loginInfo, checkedKeepLogin })
  .then((res) => {
  setIsLogin(true);
  setUserInfo(res.data);
})
  .catch((err) => {
  if (err.response.status === 401) {
    setErrorMessage('로그인에 실패했습니다.');
  }
});

axios를 사용하여 POST 요청을 보낸다. request body 에는 로그인 정보와 로그인 유지 옵션 체크값을 보낸다.

응답으로 돌아오는 (비밀번호가 제거 된) 사요자 정보를 받아 userInfo 를 업데이트 해준다.

🎁 axios


⬜ 로그인 상태 유지 체크

<label className='checkbox-container'>
  <input type='checkbox' onChange={() => setCheckedKeepLogin(!checkedKeepLogin)} />
{' 로그인 상태 유지하기'}
</label>

이것도... 내 자신이 너무 바보 같고 웃긴 게, 늘 true/false 상태값을 업데이트 할 때 if문을 썼었다. 그냥 ! 연산자 하나만 붙이면 저렇게 보기 좋은 걸


⬜ 에러 메세지 출력

const [errorMessage, setErrorMessage] = useState('');

{errorMessage ? (
  <div id='alert-message' data-testid='alert-message'>
  {errorMessage}
  </div>
 ) : (
   ''
 )}

에러 메세지를 이렇게 상태로 관리하는 게 되게 마음에 들었다. 물론 당연한 거라고 생각할 수도 있겠지만...

놀랍게도 나는 여러 에러 alert를 보여주는 친절함을 홈페이지에 넣은 적이 없었고, 늘 alert() 함수에 제각기 문자열을 보냈기 때문이다.

이런 재미로 다른 사람 코드나 래퍼런스 참고하는거겠지 뭐...



🟣 마이페이지

⬜ 로그인 하기 전

App.js에서 useEffect() 를 사용하여 제일 먼저 로그인 여부 체크 함수 authHandler를 실행한 뒤, 로그인이 되어있으면 회원 정보 화면을, 되어있지 않으면 로그인 입력 화면을 띄우는 조건부 렌더링을 실행한다.

const authHandler = () => {
    axios
      .get('http://localhost:4000/userinfo')
      .then((res) => {
        setIsLogin(true);
        setUserInfo(res.data);
      })
      .catch((err) => {
        if (err.response.status === 401) {
          console.log(err.response.data);
        }
      });
  };

난 여태 localStorage 등 클라이언트가 인증 정보를 갖고있지 않으면 로그인 화면을 보여줬는데, 저렇게 사용자 정보를 GET 요청하여 체크하는 방식을 주로 쓰는 건가 궁금해졌다.

뭐 여러가지 방법이 있고 과제 내용 흐름에 맞게 짜여진 래퍼런스라 그런 걸 수도 있지만.


⬜ 로그인 한 후

<Route
  path='/'
  element={
    isLogin ? (
    <Mypage/>
  ) : (
    <Login/>
    )
    }
/>

로그인이 되었으면 isLogintrue일 거고 아니면 false 일 것이다.

코드의 작은 부분마다 내가 지금껏 해온 방식과 다르고 훨씬 깔끔하고 상향 버전이라 부끄러워지는데,

메인 화면이 로그인 화면인 사이트를 만들 때 나는 저렇게 라우트 안에서 삼항연산자를 사용하여 화면을 전환하는 방식은 쓰지 않았던 것 같다.

냉장고를 부탁해! 를 만들 때 로그인 한 유저라면 화면을 이동하고, 아니면 로그인 화면을 보여줬었는데

대체 어떻게 했었는지 기억이 안 난다.. 다시 찾아봐야겠다.

🌠 <MyPage> & <Login> 페이지에 넘겨줘야 하는 파라미터

  • <MyPage> 의 경우
    setIsLogin : 로그아웃 시 false 값으로 변경해야 한다.
    setUserInfo : 로그아웃 시 빈 객체로 변경해야 한다.
    userInfo : GET 요청 후 받은 사용자 정보를 출력해야 하므로 보내준다.

  • <Login> 의 경우
    setIsLogin : 로그인 성공 시 true 값으로 변경한다.
    setUserInfo : 로그인 성공 시 사용자 정보를 업데이트 한다.



profile
다 하자

0개의 댓글