01. express-session
- 1) express-session이 무엇인가요?
Cookie-Session 흐름
출처: https://www.wisecleaner.com/think-tank
💡 **express-session**은 Express.js에서 **세션(Session)** 기능을 쉽게 구현하기 위한 미들웨어입니다.
저희는 이전 주차에 **쿠키(Cookie)와 세션(Session)**에 대한 개념을 배웠습니다.
**세션(Session)**을 사용하기 위해서는 **사용자 인증**과 **세션 스토리지**를 통해 사용자 장보를 저장하고,세션 정보가 담긴 **쿠키를 사용자에게 발급**하는 과정이 필요하였는데요. **express-session**은 이런 복잡한 과정을 생략하여 **간단하게 세션 기능을 구현**할 수 있도록 도와주는 미들웨어입니다.
- 2) express-session 시작하기
💡 **[express-session](https://github.com/expressjs/session)**은 **세션 ID를 클라이언트에게 발급**하고, 이 세션 ID를 통해 서버는 **클라이언트의 상태를 추적할 수 있습니다.**
즉, 클라이언트가 **세션 ID**를 발급받은 후에는 모든 서버 요청마다 **세션 ID**가 포함된 쿠키를 전달하게 되며, 이로 인해 서버는 클라이언트를 쉽게 식별할 수 있게 되는것이죠.
→ 이전에 구현하였던 Session 객체에 **세션 ID**를 저장한 것과 동일한 방법이랍니다. 😉
그러면 이제, 하나의 예시를 바탕으로 **express-session**의 동작 방식을 확인해보도록 하겠습니다!
- 📚 **Express-session Project API 명세서**
[제목 없는 데이터베이스](https://www.notion.so/bb72fc41be1342e4a36a6758333b9878?pvs=21)
- **[코드스니펫] yarn Package 설치**
```bash
# express, express-session를 설치합니다.
yarn add express express-session
```
- **[코드스니펫] express-session, app.js 템플릿**
```jsx
// app.js
import express from 'express';
import expressSession from 'express-session';
const app = express();
const PORT = 3019;
app.use(express.json());
app.use(
expressSession({
secret: 'express-session-secret-key.', // 세션을 암호화하는 비밀 키를 설정
resave: false, // 클라이언트의 요청이 올 때마다 세션을 새롭게 저장할 지 설정, 변경사항이 없어도 다시 저장
saveUninitialized: false, // 세션이 초기화되지 않았을 때 세션을 저장할 지 설정
cookie: {
// 세션 쿠키 설정
maxAge: 1000 * 60 * 60 * 24, // 쿠키의 만료 기간을 1일로 설정합니다.
},
}),
);
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
```
- **✅ express-session 미들웨어의 구성요소 알아보기**
**express-session**은 아래와 같이 **전역 미들웨어**로 등록됩니다.
```jsx
app.use(
expressSession({
secret: 'express-session-secret-key.', // 세션을 암호화하는 비밀 키를 설정
resave: false, // 클라이언트의 요청이 올 때마다 세션을 새롭게 저장할 지 설정, 변경사항이 없어도 다시 저장
saveUninitialized: false, // 세션이 초기화되지 않았을 때 세션을 저장할 지 설정
cookie: {
// 세션 쿠키 설정
maxAge: 1000 * 60 * 60 * 24, // 쿠키의 만료 기간을 1일로 설정합니다.
},
}),
);
```
각각의 구성요소는 아래와 같은 내용을 담고있습니다.
- `secret`
- **세션 ID**를 암호화하기 위한 **비밀 키**입니다.
- 클라이언트에게 발급할 **세션 ID**를 암호화하기 위한 **비밀 키** 정보입니다.
- `resave`
- 클라이언트의 **요청(Request)**이 들어올 때마다 세션 정보를 다시 저장할 지 설정합니다.
- 변경사항이 없더라도 `true` 로 설정하면, 매번 새로운 세션에 저장됩니다.
- `saveUninitialized`
- `req.session`에 아무런 정보가 저장이 되지 않더라도 사용자에게 **세션 ID**를 발급할 지 설정합니다.
- `true` 로 설정하면, 서버에 접속하는 **모든 사용자에게 세션 ID가 발급**됩니다.
- `cookie.maxAge`
- **세션 ID**가 저장된 클라이언트의 쿠키 만료 기간을 설정합니다.
[→ express-session의 설정 정보에 대해 알아보고 싶다면, 여기를 클릭하세요!](https://github.com/expressjs/session#options)
- 3) express-session API 만들기
👉 먼저, `POST` `/sessions` API를 호출했을 때 전달 받은 `userId`를 세션에 저장하고, `GET` `/sessions` API를 호출했을 때 클라이언트의 세션 정보를 출력하는 API를 구현해보도록 하겠습니다.
- `POST` `/sessions` API 만들기
```jsx
/** 세션 등록 API **/
app.post('/sessions', (req, res, next) => {
const { userId } = req.body;
// 클라이언트에게 전달받은 userId를 세션에 저장합니다.
req.session.userId = userId;
return res.status(200).json({ message: '세션을 설정했습니다.' });
});
```
- `req.session`은 클라이언트의 세션 정보를 관리하는 데 사용되는 객체입니다.
- 클라이언트의 요청이 들어온다면, `req.session.userId`에 원하는 정보를 저장합니다.
- `userId` 대신 다른 이름을 사용하려면 `req.session.<원하는 프로퍼티 명>`의 형식으로 사용하면 된답니다. 😎
- `GET` `/sessions` API 만들기
```jsx
/** 세션 조회 API **/
app.get('/sessions', (req, res, next) => {
return res.status(200).json({
message: '세션을 조회했습니다.',
session: req.session.userId ?? null, // 세션에 저장된 usrId를 조회합니다.
});
});
```
- **express-session**은 클라이언트가 전달한 쿠키의 **세션 ID**를 바탕으로 `req.session`에서 정보를 조회합니다.
- 만약, 클라이언트가 제공한 **세션 ID**에 일치하는 세션이 없을 경우, `null`을 반환합니다.
**express-session**은 클라이언트의 **요청(Request)**에 `req.session` 정보를 저장할 수 있고, 이후 클라이언트 **요청**에서 세션에 저장된 정보를 `req.session`에서 참조하여 사용할 수 있게 됩니다.
이 방식은 기존에 **JWT**를 이용한 쿠키를 클라이언트에게 전달하는 것보다 더욱 편리하게 구현할 수 있다는 점이 가장 큰 장점입니다.
⚠️ 그러나, **express-session**의 정보는 서버가 종료되면 사라지는 가장 큰 문제가 존재합니다. 이는 세션 정보가 **인 메모리(In-Memory)방식**으로 저장되기 때문인데요. 이로 인해 서버가 **재시작** 되거나 **중지**될 때 마다 **모든 세션 정보가 사라지게 됩니다.**
이런 상황을 방지하기 위해, **[Redis](https://redis.io/)**와 같은 **캐시 메모리 데이터베이스**를 이용해 세션 정보를 영구적으로 저장하여 관리하기도 한답니다. 😉
- 4) Insonnia로 API 테스트하기
- 1) Insomnia 에서 Http Request를 생성하고,
POST /sessions API를 호출
👉 `POST` `/sessions` API를 호출하여 세션 ID가 담긴 쿠키를 발급받아 봅시다!

64c47134ad123e335d76fff5의 userId를 가지도록 토큰을 발행합니다.

세션을 설정했습니다는 API Response를 받았습니다.

세션 ID가 저장된 connect.sid 이름를 가지는 Cookie를 전달 받았습니다.
- `POST` `/sessions` API를 호출했을 때, *“세션을 설정했습니다.”*는 응답을 확인할 수 있습니다.
- **Cookies** 탭에서 `connect.sid` 라는 이름의 쿠키를 전달 받은것을 확인할 수 있습니다.
- 이 쿠키는 클라이언트의 **세션 ID**를 가지고 있습니다.
- 2) Insomnia에서 Http Request를 생성하고,
GET /sessions API를 호출
👉 `POST` `/sessions` API에서 전달 받은 `connect.sid` 쿠키를 사용하여 `GET` `/sessions` API를 호출해보도록 하겠습니다!

GET /sessions API를 호출합니다.

세션 ID를 이용해 서버에 저장된 userId를 확인할 수 있습니다.
- `GET` `/sessions` API를 호출하면, *“**세션을 조회했습니다.**”*는 응답을 확인할 수 있습니다.
- 서버는 클라이언트가 전달한 `connect.sid` 쿠키에 저장된 **세션 ID**를 바탕으로 서버의 세션 정보를 조회하게 됩니다.
- 3)
GET /sessions API의 세션 에러 확인하기
👉 이번에는 서버를 종료한 후 `GET` `/sessions` API를 호출해보겠습니다. 서버가 종료되면 세션 정보가 어떻게 처리되는지 확인해보겠습니다!

서버를 종료 후 재실행합니다.

GET /sessions API를 호출합니다.

session 정보가 삭제된 응답이 발생하는 것을 확인할 수 있습니다.
- **express-session**은 서버가 종료될 경우 메모리에 저장된 세션 정보를 초기화합니다.
- 서버를 재실행한 후 `GET` `/sessions` API에서 **세션 ID**와 일치하는 세션 정보를 찾을 수 없어, 응답값에 세션 정보가 `null`로 반환된 것을 확인할 수 있습니다.
→ 이런 문제를 해결하기 위해 **외부 세션 스토리지**를 사용하는 것을 이전에 잠깐 이야기했었죠? 😉
02. [게시판 프로젝트] express-session 리팩토링
- 1) [게시판 프로젝트] express-session 시작하기
👉 이번 섹션에서는 [게시판 프로젝트]의 **로그인** 기능과 **사용자 인증 미들웨어**를 **express-session**을 이용해 리팩토링 할 예정입니다.
먼저, 아래 코드스니펫을 이용해 **express-session** 패키지를 설치하고, 전역 미들웨어에 등록해보도록 하겠습니다.
- **[코드스니펫] express-session 설치 명령어**
```bash
# express-session 미들웨어를 설치합니다.
yarn add express-session
```
- **[코드스니펫] [게시판 프로젝트] express-session 등록하기 - app.js**
```jsx
// src/app.js
import express from 'express';
import cookieParser from 'cookie-parser';
import expressSession from 'express-session';
import LogMiddleware from './middlewares/log.middleware.js';
import ErrorHandlingMiddleware from './middlewares/error-handling.middleware.js';
import UsersRouter from './routes/users.router.js';
import PostsRouter from './routes/posts.router.js';
import CommentsRouter from './routes/comments.router.js';
const app = express();
const PORT = 3018;
app.use(LogMiddleware);
app.use(express.json());
app.use(cookieParser());
app.use(
expressSession({
secret: 'customized_secret_key', // 세션을 암호화하는 비밀 키를 설정
resave: false, // 클라이언트의 요청이 올 때마다 세션을 새롭게 저장할 지 설정, 변경사항이 없어도 다시 저장
saveUninitialized: false, // 세션이 초기화되지 않았을 때 세션을 저장할 지 설정
cookie: {
// 세션 쿠키 설정
maxAge: 1000 * 60 * 60 * 24, // 쿠키의 만료 기간을 1일로 설정합니다.
},
}),
);
app.use('/api', [UsersRouter, PostsRouter, CommentsRouter]);
app.use(ErrorHandlingMiddleware);
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
```
- 2) [게시판 프로젝트] 로그인 리팩토링
💡 **[게시판 프로젝트] 로그인 API 변경 전 비즈니스 로직**
1. `email`, `password`를 **body**로 전달받습니다.
2. 전달 받은 `email`에 해당하는 사용자가 있는지 확인합니다.
3. 전달 받은 `password`와 데이터베이스에 저장된 `password`를 **bcrypt**를 이용해 검증합니다.
4. 로그인에 성공한다면, 사용자에게 JWT를 발급합니다.
이전에는 4️⃣ 에서 사용자가 로그인 성공 시, **JWT를 생성**하고 이를 **쿠키로 전달**하였습니다.
하지만, 저희는 이제 JWT 대신 **express-session**의 **세션 ID**를 사용해보려고 합니다. 로그인이 성공하면 **세션 ID**를 생성하고, 이를 **쿠키로 사용자에게 전달**할 예정입니다.
- **[코드스니펫] [게시판 프로젝트] 로그인 리팩토링**
```jsx
// src/routes/users.router.js
/** Express-Session 로그인 API **/
router.post('/sign-in', async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await prisma.users.findFirst({ where: { email } });
if (!user)
return res.status(401).json({ message: '존재하지 않는 이메일입니다.' });
// 입력받은 사용자의 비밀번호와 데이터베이스에 저장된 비밀번호를 비교합니다.
else if (!(await bcrypt.compare(password, user.password)))
return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });
// 로그인에 성공하면, 사용자의 userId를 바탕으로 세션을 생성합니다.
req.session.userId = user.userId;
return res.status(200).json({ message: '로그인 성공' });
} catch (err) {
next(err);
}
});
```
로그인이 성공하면 `userId`를 이용해 JWT를 생성하고, 이를 **쿠키로 사용자에게 전달**하는 과정이 삭제되었습니다. 대신에, `req.session.userId`를 사용하여, 사용자의 세션 정보에 `userId`를 할당하도록 수정하였습니다.
이렇게 변경한다면, 쿠키 내부가 어떤 값을 가지는지 알 수 없어도, 클라이언트가 다음 요청을 보낼 때마다 **세션 ID를 바탕으로 사용자 정보를 조회**할 수 있게 될 것입니다. 😎
- 3) [게시판 프로젝트] 사용자 인증 미들웨어 리팩토링
💡 **[게시판 프로젝트] 사용자 인증 미들웨어 변경된 비즈니스 로직**
1. 클라이언트로부터 **세션 ID**를 전달받습니다.
2. 세션 정보에 저장된 `userId`를 이용해 사용자를 조회합니다.
3. `req.user` 에 조회된 사용자 정보를 할당합니다.
4. 다음 미들웨어를 실행합니다.
기존 사용자 인증 미들웨어는 **쿠키를 조회**하고, **Bearer Token과 JWT를 검증**하는 복잡한 비즈니스 로직을 수행하였습니다. 하지만, **express-sesison**을 도입하게 된다면, 이러한 복잡한 로직 없이도 세션 정보를 활용해 사용자를 식별할 수 있게 될 것입니다.
따라서, 개선된 사용자 인증 미들웨어는 세션 정보에 저장된 `**userId**`를 이용해 **사용자를 조회**하고, 이를 `**req.user`에 할당**하는 간단한 역할만을 담당하게 됩니다. 이로 인해, 사용자 인증 미들웨어의 **코드 복잡도를 줄이며, 인증 과정은 더욱 단순**해지게 될 것입니다.
그렇다면, 변경된 비즈니스 로직을 바탕으로 사용자 인증 미들웨어를 리팩토링 해볼까요?
- **[코드스니펫] [게시판 프로젝트] 사용자 인증 미들웨어 리팩토링**
```jsx
// src/middlewares/auth.middleware.js
import { prisma } from '../utils/prisma/index.js';
export default async function (req, res, next) {
try {
const { userId } = req.session;
if (!userId) throw new Error('로그인이 필요합니다.');
const user = await prisma.users.findFirst({
where: { userId: +userId },
});
if (!user) throw new Error('토큰 사용자가 존재하지 않습니다.');
// req.user에 사용자 정보를 저장합니다.
req.user = user;
next();
} catch (error) {
return res
.status(401)
.json({ message: error.message ?? '비정상적인 요청입니다.' });
}
}
```
**사용자 인증 미들웨어**의 복잡한 비즈니스 로직이 `req.session`의 정보를 조회하는 간단한 행위로 변경되었습니다. 또한, **JWT 에러 처리**또한 `try/catch` 구문을 통해 여러 에러를 처리하던 방식이 단순하게 하나의 에러만 출력되도록 수정된 것이죠. ☺️
- 4) [게시판 프로젝트] express-session MySQL
👉 [**express-mysql-session**](https://github.com/chill117/express-mysql-session) 모듈은 express-session의 세션 정보를 MySQL에 저장할 수 있도록 도와주는 모듈입니다.
지금까지 **express-session**을 이용해 리팩토링하던 중 한 가지 문제점이 발생하였는데요. 바로 **서버를 재실행할 때마다 세션 정보가 초기화**되는 문제입니다.
이는 express-session이 기본적으로 **세션 정보를 인 메모리(In-Memory)에 저장**하기 때문에 발생하는 문제로, 서버가 종료될 때 마다 이전에 생성된 세션 정보가 사라지게 되는 것이죠.
이러한 문제를 해결하기 위해, **외부 세션 스토리지**를 사용하여 서버가 종료되더라도 사용자의 세션 정보가 외부 스토리지에 영구적으로 남을 수 있도록 리팩토링을 해야하는데요.
저희는 **MySQL**를 **외부 세션 스토리지**로 사용하여, 사용자의 세션 정보를 MySQL에 저장하고 관리할 수 있도록 프로젝트를 개선할 예정입니다.
가장 먼저, **express-mysql-session** 모듈을 설치하고 해당 옵션들을 설정해봅시다.
- **[코드스니펫] [게시판 프로젝트] express-mysql-session 설치 명령어**
```bash
# 외부 세션 스토리지를 사용하기 위한, express-mysql-session 모듈을 설치합니다.
yarn add express-mysql-session
```
- **[코드스니펫] [게시판 프로젝트] express-mysql-session 적용하기**
```jsx
// src/app.js
import express from 'express';
import cookieParser from 'cookie-parser';
import expressSession from 'express-session';
import expressMySQLSession from 'express-mysql-session';
import LogMiddleware from './middlewares/log.middleware.js';
import ErrorHandlingMiddleware from './middlewares/error-handling.middleware.js';
import UsersRouter from './routes/users.router.js';
import PostsRouter from './routes/posts.router.js';
import CommentsRouter from './routes/comments.router.js';
const app = express();
const PORT = 3018;
// MySQLStore를 Express-Session을 이용해 생성합니다.
const MySQLStore = expressMySQLSession(expressSession);
// MySQLStore를 이용해 세션 외부 스토리지를 선언합니다.
const sessionStore = new MySQLStore({
user: 'root',
password: 'aaaa4321',
host: 'express-database.clx5rpjtu59t.ap-northeast-2.rds.amazonaws.com',
port: 3306,
database: 'community_hub',
expiration: 1000 * 60 * 60 * 24, // 세션의 만료 기간을 1일로 설정합니다.
createDatabaseTable: true, // 세션 테이블을 자동으로 생성합니다.
});
app.use(LogMiddleware);
app.use(express.json());
app.use(cookieParser());
app.use(
expressSession({
secret: 'customized_secret_key', // 세션을 암호화하는 비밀 키를 설정
resave: false, // 클라이언트의 요청이 올 때마다 세션을 새롭게 저장할 지 설정, 변경사항이 없어도 다시 저장
saveUninitialized: false, // 세션이 초기화되지 않았을 때 세션을 저장할 지 설정
store: sessionStore, // 외부 세션 스토리지를 MySQLStore로 설정합니다.
cookie: {
// 세션 쿠키 설정
maxAge: 1000 * 60 * 60 * 24, // 쿠키의 만료 기간을 1일로 설정합니다.
},
}),
);
app.use('/api', [UsersRouter, PostsRouter, CommentsRouter]);
app.use(ErrorHandlingMiddleware);
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
```
⚠️ **express-mysql-session** 모듈의 가장 큰 문제점은 **세션 ID**로 정보를 조회할 때마다 MySQL의 조회 쿼리를 매번 실행된다는 점입니다.
이러한 문제를 해결하기 위한 다양한 방법들이 존재하는데요. 저희가 이전에 사용한 **JWT 쿠키**를 이용하는 것이 하나의 방법이고, 외부 세션 스토리지를 **캐시 메모리 데이터베이스인 Redis**로 변경하는 것도 가능한 해결책이죠.
따라서, 어떤 기술 스택을 선택할지는 **현재 상황에서 가장 효율적이고, 적합한 기술을 선택**하는 것이 가장 중요합니다.
예를 들어, 외부 세션 스토리지 사용을 원하지 않는다면, **JWT쿠키**를 사용할 수 있으며, 성능 개선이 필요하다면 **[Redis](https://redis.io/)**를 도입하는 것을 고려하는 것처럼 말이죠. 😎
- 5) [게시판 프로젝트] dotenv 적용하기