🔥 학습목표
- Cookie를 사용하여 사용자의 로그인 상태를 저장한다.
// 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 으로 제출된 응답 req
의 headers
출력 후 content-type
를 확인하면 데이터 타입을 알 수 있다.)
app.use(
cors({
origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'OPTIONS'],
credentials: true,
})
);
http://localhost:3000
출처와 리소스를 공유하며, ['GET', 'POST', 'OPTIONS']
요청만 허가한다.
다른 도메인에 요청을 보낼 때 요청에 인증(credential) 정보를 담아서 보낼 지를 결정하는 항목인 credentials
도 true
로 설정한다.
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const HTTPS_PORT = process.env.HTTPS_PORT || 4000;
인증서 파일이 존재하는 경우 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);
폴더를 이렇게 정리해놓고 index.js
파일에다
module.exports = {
login: require('./users/login'),
logout: require('./users/logout'),
userInfo: require('./users/userInfo'),
};
한꺼번에 모듈을 배포하면 가독성이 훨씬 좋다는 걸 알게되었다.
앞으로 할 프로젝트나 지금까지 해온 것들 이렇게 리팩토링 해야지
필요한 상태 관리
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
을 사용주요 기능
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
에서는 회원 정보를 보내주는 게 포인트다!
유저 정보(비밀번호 제외) 전송하기
(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
가 회원 목록에 존재하지 않는다면 에러 메세지를 보낸다.
정상적으로 존재한다면 비밀번호를 제외한 회원 정보를 요청에 대한 응답으로 보내준다.
쿠키 삭제
(req, res) => {
res
.status(205)
.clearCookie('cookieId', {
domain: 'localhost',
path: '/',
sameSite: 'strict',
secure: true,
})
.send('Logged Out Successfully');
}
205 Reset Content
- form의 내용을 지우거나 캔버스 상태를 재설정하거나 UI를 새로 고치려면 client의 문서뷰를 새로고침하라고 알려준다.const [loginInfo, setLoginInfo] = useState({
userId: '',
password: '',
});
const handleInputValue = (key) => (e) => {
setLoginInfo({ ...loginInfo, [key]: e.target.value });
};
ID, PWD를 입력받을 때 저런 식으로 상태를 덮어씌울 쑤 있는 지 몰랐다. 늘 ID input 태그의 onChange와 PWD input의 onChange를 각각 적었던 것 같은데...
그냥 키를 받아서 구조분해할당으로 덮어씌면 된다는 걸 깨달았다.
종종 이런 부분에서 머리가 왜 안 돌아갔나 싶다
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
를 업데이트 해준다.
<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/>
)
}
/>
로그인이 되었으면 isLogin
이 true
일 거고 아니면 false
일 것이다.
코드의 작은 부분마다 내가 지금껏 해온 방식과 다르고 훨씬 깔끔하고 상향 버전이라 부끄러워지는데,
메인 화면이 로그인 화면인 사이트를 만들 때 나는 저렇게 라우트 안에서 삼항연산자를 사용하여 화면을 전환하는 방식은 쓰지 않았던 것 같다.
냉장고를 부탁해! 를 만들 때 로그인 한 유저라면 화면을 이동하고, 아니면 로그인 화면을 보여줬었는데
대체 어떻게 했었는지 기억이 안 난다.. 다시 찾아봐야겠다.
🌠
<MyPage>
&<Login>
페이지에 넘겨줘야 하는 파라미터
<MyPage>
의 경우
setIsLogin : 로그아웃 시false
값으로 변경해야 한다.
setUserInfo : 로그아웃 시 빈 객체로 변경해야 한다.
userInfo : GET 요청 후 받은 사용자 정보를 출력해야 하므로 보내준다.
<Login>
의 경우
setIsLogin : 로그인 성공 시true
값으로 변경한다.
setUserInfo : 로그인 성공 시 사용자 정보를 업데이트 한다.