909 Devlog

[유튜브 클론코딩] Sessions and Cookies 본문

토이 프로젝트/유튜브 클론코딩

[유튜브 클론코딩] Sessions and Cookies

구공구 2023. 8. 1. 13:37
728x90

이번 포스팅에서는 express에서 session과 cookie를 사용하는 법에 대해 알아보겠습니다.

세션과 쿠키는 이전 포스팅 [WEB/HTTP] - 쿠키(Cookie)와 세션(Session)에서 다루었으니 이 포스팅에서는 express에서 사용하는 방법 위주로 작성하겠습니다.

1. express-session

express에서 세션을 사용하기 위해서는 express-session을 설치하고 서버 파일에 불러와야 합니다.

터미널에 아래 코드를 입력하여 설치합니다.

npm i express-session

서버 파일에 express-session을 불러오고, router 앞에 초기화합니다.

import session from "express-session";
...
app.use(
    session({
        secret: "Hello",
        resave: true,
        saveUninitialized: true,
    })
);

app.use("/", rootRouter);
...

 

위 코드를 적용시키고 페이지에 들어가서

우클릭 - 검사를 클릭하고,

애플리케이션 탭을 보면

쿠키가 생성되어 보내진 것을 확인할 수 있습니다.

브라우저를 새로고침 하면 같은 내용의 쿠키가 표시되지만 서버를 재시작하고 다시 보면 다른 쿠키가 저장된 것을 확인하실 수 있을 것입니다. 예전에 fakeDB를 사용했던 것처럼, 현재 express가 세션을 메모리에 저장하고 있어서 서버를 재시작하면 사라지게 됩니다.

1.1 req.session

데이터베이스에 세션 ID를 저장하기 전에, controller에서 유저와 로그인 여부를 저장하고 확인해 봅시다.

서버 파일에 session을 로그 하는 middleware를 만들어 session을 확인하는 코드를 작성하고,

...
app.use((req, res, next) => {
    req.sessionStore.all((error, sessions) => {
        console.log(sessions);
        next();
    });
});
...

로그인을 할 때 사용하는 post controller에서 다음코드와 같이 유저를 찾고, req.session에 정보를 저장합니다.

export const postLogin = async (req, res) => {
    const { username } = req.body;
    const pageTitle = "Login";
    const user = await UserModel.findOne({ username });
    if (!user) {
        return res.status(400).render("login", {
            pageTitle,
            errorMessage: "An account with this username does not exists.",
        });
    }
    req.session.loggedIn = true;
    req.session.user = user;
    res.redirect("/");
};

로그인 페이지로 이동해서 만들어 놓은 유저 데이터로 로그인하면

터미널에 아래와 같은 결과가 출력됩니다.

세션 데이터에 세션 ID와 loggedIn이 true로 저장되어 있는 것을 확인할 수 있습니다.

 

이제 템플릿유저의 로그인 여부에 따라 출력되는 화면을 다르게 할 수 있는지 테스트해봅시다.

		if !req.session.loggedIn 
                    li 
                        a(href="/join") Join 
                    li 
                        a(href="/login") Login

템플릿에 if문을 추가하고 페이지를 새로고침 하면 

오류가 발생하게 됩니다.

 

pug에서 req.session을 인식할 수 없어서 발생하는 오류입니다.

pug는 req.session 대신 전역변수인 res.locals를 이용할 수 있기 때문에 res.locals를 관리하는 middleware를 만들어서 위에서 하고자 했던 if문을 구현해 보겠습니다.

1.2 res.locals

middleware.js서버 파일과 같은 디렉토리에 생성하고 다음과 같은 코드를 작성했습니다.

export const localsMiddleware = (req, res, next) => {
    res.locals.loggedIn = Boolean(req.session.loggedIn);
    console.log(res.locals);
    next();
};

이제 sever.js에 localMiddleware를 import 하고 use 함수에 적용시키면,

...
import { localsMiddleware } from "./middlewares";
...
app.use(localsMiddleware);
...

페이지를 새로고침할 때마다 우리가 관리하고 사용할 수 있는 res.locals가 출력됩니다.

이제 템플릿에서도 사용할 수 있는 변수를 생성했으니, 템플릿을 수정해 보겠습니다.

                if loggedIn
                    li 
                        a(href="/logout") Log Out 
                else 
                    li 
                        a(href="/join") Join 
                    li 
                        a(href="/login") Login

res.locals의 loggedIn을 받아서, true면 log out 버튼을 출력하고, false면 join과 login을 출력합니다.

loggedIn === false
loggedIn === true

이제 MongoDB에 세션을 연결해 봅시다.

2. connect-mongo

세션을 MongoDB에 저장하기 위해서는 connect-mongo를 설치해야 합니다.

터미널에 아래 코드를 작성해서 설치합니다.

npm i connect-mongo

connect-mongo를 사용하기 위해서는 서버 파일에 MongoStore를 아래와 같이 import 하고 session의 옵션 값에 추가해야 합니다.

import MongoStore from "connect-mongo";
...
app.use(
    session({
        secret: "Hello",
        resave: true,
        saveUninitialized: true,
        store: MongoStore.create({ // <- store 옵션 추가
            mongoUrl: "mongodb://127.0.0.1:27017/nodetest",
        }),
    })
);
...

이제 페이지를 새로고침 하면 MongoDB에 sessions라는 collection이 추가된 것을 확인할 수 있습니다.

이제 서버를 재시작해도 session 정보가 데이터베이스에 남아있기 때문에, 사용자가 로그인했는지를 기억할 수 있게 되었습니다.

 

2.1 Uninitialized Sessions

1. 에서 express-session을 초기화할 때

resave: true,
saveUninitialized: true,

를 옵션으로 설정했었습니다.

 

위에서 봤듯, 쿠키를 지우고 다시 페이지에 들어가거나 다른 브라우저로 해당 페이지에 들어가면 그때마다 새로운 세션을 만들어 DB에 저장하게 될 것입니다.

 

만약 페이지에 봇들이 몰려오거나, 로그인하지 않고 그냥 구경만 하는 유저들까지 모두 세션에 저장하게 되면 서버에 많은 데이터를 저장하게 되고, 그만큼 서버 리소스를 많이 차지하여 서버 유지 비용이 늘어날 것입니다.

 

이제 옵션을 false로 바꿔봅시다.

resave: false,
saveUninitialized: false,

이제 우리가 만든 페이지에 방문해도 아무에게나 쿠키가 주어지지 않습니다.

 

우리가 바꾼 옵션에 대해 자세히 알아봅시다.

 

resave는 request에서 새로 생성된 session과, 기존에 있던 session이 똑같을 때에도 다시 저장할 것인지 설정하는 옵션입니다.

session에 변경사항이 없음에도 다시 저장하면 서버 리소스를 잡아먹어 비효율적이므로, false로 설정합니다.

(사용하는 Store에 request 마다 세션 만료일자를 업데이트해주는 기능이 따로 없으면 true로 설정하지만, 우리가 사용하는 MongoStore에서는 자동으로 만료일자를 업데이트해주기 때문에 false로 설정합니다.)

 

다음으로, saveUninitialized의 "uninitialized"는 세션이 새로 만들어지고 수정된 적이 없을 때를 나타내는 상태입니다.

그러니까 saveUninitialized는 말 그대로 "초기화되지 않은 세션을 저장"할지 설정하는 옵션이므로 true일 때는 모든 세션을 DB에 저장하고, false일 때는 초기화된 세션만 DB에 저장하게 됩니다.

 

따라서 우리가 userConroller.js 파일의 postLogin controller에서 req.session을 초기화했으니, postLogin을 거친 세션만 저장하게 되는 것입니다.

backend가 로그인한 사용자에게만 쿠키를 주도록 설정되었다는 말이죠.

 

 

한번 session DB를 비우고 수정한 옵션이 잘 작동되는지 확인해 봅시다.

지금 제 session DB는 비어있는 상태입니다.

이 상태에서 페이지에 들어가도

원래는 그냥 저장되던 세션이, 이제는 DB에 저장되지 않습니다.

이제 로그인을 해보면

의도한 대로 세션이 DB에 저장됩니다.

이제 backend는 기억하고 싶은 사람들에게만 쿠키를 줘서 서버 비용을 줄일 수 있게 되었습니다.

3. More about Cookie

위 사진처럼 쿠키의 프로퍼티를 보면

Name, Value, Domain, Path, Expires / Max-Age, Size, HttpOnly, Secure, SameSite, Priority들이 있습니다.

 

중요한 것만 살펴봅시다.

쿠키의 Secure, 옵션 값인 secret은 우리가 쿠키에 sign 할 때 사용하는 프로퍼티입니다.

쿠키에 sign 하는 이유는 backend가 쿠키를 줬다는 것을 보여주기 위함입니다.

이 옵션을 사용하는 이유는 session hijack(공격자가 다른 사용자의 쿠키를 훔쳐서 마치 그 사용자인 척 페이지에 접속하는 공격)이라는 공격유형에 대응하기 위해서 사용합니다.

 

따라서 임시로 적용해 놓은 Hello가 아닌 길고 강력한 무작위 string을 만들어야 합니다.

 

다음으로, Domain은 이 쿠키를 만든 backend가 누구인지 알려주는 프로퍼티입니다.

예를 들어 Google, Youtube, Instagram처럼 domain에 따라 쿠키를 저장합니다.

브라우저는 해당 domain에 접속할 때만 해당 쿠키를 서버에 보내게 됩니다.

 

또, Expires / Max-Age는 쿠키 만료 날짜를 나타내는 프로퍼티입니다.

지금은 값이 "Session"으로 적혀있습니다. 이 값은 쿠키 만료 날짜가 명시되지 않았다는 것을 나타냅니다.

그런데 expires라는 값은 세션 DB에서 봤었습니다. express-session에서 기본값으로 2주를 설정하고, 우리가 수정할 수도 있습니다.

app.use(
    session({
        secret: "Hello",
        resave: false,
        saveUninitialized: false,
        cookie: {
            maxAge: 20000,
        },
        store: MongoStore.create({
            mongoUrl: "mongodb://127.0.0.1:27017/nodetest",
        }),
    })
);

위와 같이 설정하면, maxAge는 20000밀리세컨드로 설정되어, 세션이 저장되고 20초가 지나면 자동으로 세션이 만료될 것입니다.

저는 그냥 기본값 2주로 설정하기 위해, cookie 옵션을 지우겠습니다.

설정해 놓은 session 옵션들을 보면, secretstore가 있습니다.

이 웹사이트를 서버에 배포할 때, 해당 옵션들은 외부에서 볼 수 없도록 보호해야 합니다.

지금 store에 설정해 놓은 URL은 localhost라 각자 다 다르니까 괜찮지만, 배포할 URL은 localhost가 아니라서 다른 누군가가 DB에 접근해 username과 password를 볼 수도 있기 때문입니다.

 

따라서, 위와 같은 공개하지 않을 값들을 보관할 environment file(환경변수 파일)을 만들겠습니다.

프로젝트 파일의 최상단 디렉토리에 .env 파일을 만들겠습니다.

그리고 깃허브에 공유하지 않도록 .gitignore 파일에 .env를 추가하겠습니다.

이제 .env에 코드에 들어가면 안되는 값들을 추가해 보겠습니다.

관습적으로, .env 파일에 추가하는 모든 것은 대문자로 작성합니다.

COOKIE_SECRET 값은 session 옵션에 secret 설정 값으로 사용될 값입니다.

예측할 수 없도록 아무 패턴 없이 그냥 막 두드렸습니다.

DB_URL은 store 옵션과 db.js 파일에 쓰일 데이터베이스 URL입니다 각자 사용하시는 DB의 URL을 작성해 주시면 됩니다.

 

.env 파일 속의 값들은 process.env.변수 이름으로 사용합니다.

secret과 store의 URL과 db.js 파일에서 사용하는 URL 주소를 수정해 줍시다.

app.use(
    session({
        secret: process.env.COOKIE_SECRET,
        resave: false,
        saveUninitialized: false,
        store: MongoStore.create({
            mongoUrl: process.env.DB_URL,
        }),
    })
);
// db.js
...
mongoose.connect(process.env.DB_URL);
...

 

코드를 저장하면 콘솔에 process.env.변수 이름 변수가 저장되지 않았다는 에러가 뜨게 될 것입니다.

따라서 .env 파일을 사용할 수 있도록 해주는 패키지를 설치해야 합니다.

아래서 설치할 패키지는 .env 파일에 작성되어 있는 변수들을 Node.js의 process 환경인 process.env 안에 넣어주는 역할을 합니다.

 

터미널에 아래 코드를 입력해 dotenv 패키지를 설치합시다.

npm i dotenv

dotenv 패키지는 우리가 만든 app안에서 최대한 먼저 호출해야 합니다.

따라서 이 프로젝트를 시작하는 파일인 init.js의 최상단에 코드를 작성하여 dotenv를 불러오겠습니다.

// init.js

import "dotenv/config";
import "./db";
import "./models/Video";
import "./models/User";
import app from "./server";
...

그럼 이제 코드가 정상적으로 작동되어 오류 없이 서버가 동작하고, env 파일에 작성되어있는 변수를 사용할 수 있게 되었습니다.

728x90