🔥 학습목표
- 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 : 로그인 성공 시 사용자 정보를 업데이트 한다.