Express Session

윤태규·2024년 1월 10일

01. express-session

  • 1) express-session이 무엇인가요? Cookie-Session 흐름
  출처: [https://www.wisecleaner.com/think-tank](https://www.wisecleaner.com/think-tank/292-What-are-Cookie--Session.html) 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를 가지도록 토큰을 발행합니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7478d0a3-2793-43c9-90c6-58fcebcae7c7/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-08-20_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_6.04.06.png) 64c47134ad123e335d76fff5의 userId를 가지도록 토큰을 발행합니다. ![세션을 설정했습니다는 API Response를 받았습니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3ae36182-8d56-44d6-8712-aa98fda07b1b/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-08-06_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_6.11.41.png) 세션을 설정했습니다는 API Response를 받았습니다. ![세션 ID가 저장된 connect.sid 이름를 가지는 Cookie를 전달 받았습니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b557c37a-a889-4e90-97b2-1d86da410bfe/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-08-20_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_6.04.30.png) 세션 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를 호출합니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fe894d9-9053-4d43-ae36-bc3685363795/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-08-20_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_6.05.41.png) GET /sessions API를 호출합니다. ![세션 ID를 이용해 서버에 저장된 userId를 확인할 수 있습니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e25e1258-0565-4c6e-9a43-fd9a506fd326/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-08-06_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_6.12.25.png) 세션 ID를 이용해 서버에 저장된 userId를 확인할 수 있습니다. - `GET` `/sessions` API를 호출하면, *“**세션을 조회했습니다.**”*는 응답을 확인할 수 있습니다. - 서버는 클라이언트가 전달한 `connect.sid` 쿠키에 저장된 **세션 ID**를 바탕으로 서버의 세션 정보를 조회하게 됩니다.
    • 3) GET /sessions API의 세션 에러 확인하기 👉 이번에는 서버를 종료한 후 `GET` `/sessions` API를 호출해보겠습니다. 서버가 종료되면 세션 정보가 어떻게 처리되는지 확인해보겠습니다! ![서버를 종료 후 재실행합니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/977d2e49-9fa9-4ec1-b9c0-2dc394d6eb8e/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-08-06_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_6.17.39.png) 서버를 종료 후 재실행합니다. ![GET /sessions API를 호출합니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b7e46b2a-31b4-452b-a087-0c4bb04cef08/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-08-20_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_6.05.41.png) GET /sessions API를 호출합니다. ![session 정보가 삭제된 응답이 발생하는 것을 확인할 수 있습니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c5dd2a3c-7cbf-46bb-b1f8-1f77db05d9ae/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2023-08-20_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_6.07.03.png) 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 적용하기
profile
끝까지 가자

0개의 댓글