[session 로그인] (3) 회원가입/로그인 구현 - 세션 데이터 추가하기 (타입스크립트)

sookyoung.k·2024년 2월 21일
1

🍏 NodeJS

목록 보기
9/10
post-thumbnail

[session 로그인] (2) express-session 세션 설정에 이어서 회원가입과 로그인에 세션을 어떻게 사용하는지 이어서 글을 작성하겠다.

🎟️ 회원가입

정말 간단하고 기본적인 회원 가입 절차를 만들어볼 예정이다.

🎡 USER 테이블 생성

회원 정보를 저장할 DB 테이블 이름은 User로 정했고, user_id(회원 아이디) 값과 name(이름), password(비밀번호) 정보를 저장할 것이다.

import { Model, DataTypes, Sequelize } from 'sequelize';

export default class user extends Model {
  public id!: number;
  public user_id!: string;
  public user_name!: string;
  public user_password!: string;

  public static initModel(sequelize: Sequelize) {
    return user.init(
      {
        user_id: {
          type: DataTypes.STRING(30),
          allowNull: false,
        },
        user_name: {
          type: DataTypes.STRING(30),
          allowNull: false,
        },
        user_password: {
          type: DataTypes.STRING(100),
          allowNull: false,
        },
      },
      {
        sequelize,
        timestamps: false,
        underscored: true,
        paranoid: false,
        modelName: 'User',
        tableName: 'user',
        charset: 'utf8mb4',
        collate: 'utf8mb4_general_ci',
      },
    );
  }

  public static associate(db: any) {}
}

시퀄라이즈를 사용하여 테이블을 생성했다.

시퀄라이즈는 Node.js에서 MySql을 사용할 때 쿼리를 더 쉽게 사용할 수 있도록 도와주는 ORM(Object-Relational Mapping)이라고 한다.

* ORM - 객체와 관계형 데이터베이스의 관계를 매핑해주는 도구

🎠 컬럼 선언

  public id!: number;
  public user_id!: string;	// 회원가입 시 유저가 생성하는 아이디
  public user_name!: string;	// 유저 이름
  public user_password!: string;	// 비밀번호 

먼저 필요한 컬럼을 선언한다. id는 자동으로 생성될 인덱스 번호이며 데이터 타입은 number이다.

🎠 컬럼 설정

return user.init(
      {
        user_id: {
          type: DataTypes.STRING(30),
          allowNull: false,
        },
        user_name: {
          type: DataTypes.STRING(30),
          allowNull: false,
        },
        user_password: {
          type: DataTypes.STRING(100),
          allowNull: false,
        },
      },
      {
        sequelize,
        timestamps: false,
        underscored: true,
        paranoid: false,
        modelName: 'User',
        tableName: 'user',
        charset: 'utf8mb4',
        collate: 'utf8mb4_general_ci',
      },
    );

user 테이블에서 init() 메서드를 통해 테이블과 컬럼의 상세 설정을 해준다. 두 개의 인자가 필요하다.

첫 번째 인자로는 컬럼의 데이터 타입, 크기 등이 온다. 시퀄라이즈에서 index에 해당하는 id 값은 특별하게 명시되어있는 것이 없으면 자동으로 생성된다. 하지만 만약 id가 아닌 다른 값을 index로 주고 싶다면 따로 설정이 가능하다.
현재 아주 간단하고 기본적인 유저 테이블을 생성할 것이기 때문에 데이터 타입과 크기, not null 옵션만 준 상태이다.

두 번째 인자로는 테이블 설정이다. 여기서는 시퀄라이즈에 대한 내용이 주가 아니기 때문에 자세한 설명은 하지 않도록 하겠음!

🪪 회원가입 절차

일반적으로는 회원가입 시에 아이디(혹은 이메일)와 비밀번호를 입력하는 것이 대부분이겠지만, 여기선 아이디와 이름만을 받는다. 그렇게 됐다... 비밀번호는 가입과 동시에 임시 비밀번호를 부여받고, 이후에 변경이 가능하게 만들 생각이다.

export const regist = async (req: Request, res: Response) => {
  try {
    const { id, name }: IUser = req.body;

    const existUser = await User.findOne({ where: { user_id: id } });

    if (existAccount)
      res.status(400).json({ message: '이미 존재하는 아이디입니다.' });
    else {
      const hashedPassword = bcrypt.hashSync(
        process.env.DEFAULT_PASSWORD,
        bcrypt.genSaltSync(),
      );
      await User.create({
        user_id: id,
        user_name: name,
        user_password: hashedPassword,
      });
      res.json({ message: '회원 등록을 성공적으로 마쳤습니다.' });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ error, message: '회원 등록에 실패했습니다.' });
  }
};
  1. 회원 정보 입력

🪄 회원 정보 (id, name)가 req.body에 담겨서 서버에 전달된다.

  1. 중복 확인
User.findOne({ where: { user_id: id } })

🪄 유저가 입력한 id 값을 통해 이미 등록된 회원인지(DB에 해당 id값이 존재하는지)를 확인한다.

🪄 회원 정보가 존재할 경우 (이미 사용중인 id인 경우) 에러 메시지를 반환하고, 중복회원이 아닐 경우 다음 절차로 넘어간다.

  1. 비밀번호 암호화
      const hashedPassword = bcrypt.hashSync(
        process.env.DEFAULT_PASSWORD,
        bcrypt.genSaltSync(),
      );

비밀번호 암호화는 bcrypt 라이브러리를 사용했다. 암호화 방법 대한 내용은 이전 글에서 bcrypt 모듈 사용법에 대해 작성한 바가 있으므로 생략!

회원가입 시 자동으로 부여되는 기본 비밀번호는 env 파일에 넣어서 사용하고 있다.

실제로 코드를 작성할 때 엔브 파일에 넣는거 깜빡해서 산업스파이라는 이야길 들었다 ㅋㅋㅋㅋ 개웃김 ㅋㅋㅋㅋ (실제 상황이었다면 안웃김...)

  1. User 테이블에 회원 정보 등록 (Insert)
      await User.create({
        user_id: id,
        user_name: name,
        user_password: hashedPassword,
      });

유저가 입력한 id 값과 name, 그리고 암호화 된 hashedPassword를 등록한다.


여기까지가 회원가입 과정이다. 회원가입 절차까지는 시퀄라이즈에 대한 기본 문법만 알고 있다면 어려울 것이 없다능,,,

다음은 로그인!!
회원 정보를 확인한 다음 로그인에 성공할 경우 세션에 회원을 식별할 수 있는 데이터를 저장하는 것이 목표이다.

🎫 로그인

로그인은 여러모로 까다로운 지점이 많았다. session을 사용해서 로그인을 구현하는 것이 처음이었기 때문에 난관을 예상하긴 했지만, 문제 하나를 해결하면 또 다른 문제가 생겨나고, 해결하면 또 생기고... 반복이었다. 하다가 열받아서 애꿎은 쿠키만 아그작아그작 씹어먹음...

주요 이슈로는

세션 정보 저장(타입스크립트 사용 시 특히 주의), 세션 목록 조회

가 있었다.

일단은 코드를 작성하면서 내가 맞닥뜨린 이슈들을 하나하나씩 살펴보도록 하겠다. 글이 길어질 수도 있을 것 같음.

🎢 전체 코드

export const login = async (req: Request, res: Response) => {
  try {
    const { user_id, password } = req.body;
    
    const isUser = await User.findOne({ where: { user_id } });
    if (!isUser || !bcrypt.compareSync(password, isUser.user_password)) {
      return res.status(400).json({ message: '회원 정보가 존재하지 않습니다.' });
    }

    const isSessionId = await findSessionId(user_id);
    
    if (isSessionId) {
      console.log('중복 로그인 → 기존 세션을 삭제하고 세션을 재생성합니다.');
      req.sessionStore.destroy(isSessionId.split('.')[0]);
      fs.rmSync(`${SESSION_PATH}/${isSessionId}`);
      req.session.regenerate((err) => {
        if (err) res.json({ message: '세션을 재생성하는데 실패했습니다.' });
        else {
          req.session.user_id = isUser.user_id;
          req.session.user_name = isUser.user_name;

          return res.json({
            message: '로그인 성공',
            expiredTime: req.session.cookie.expires.getTime(),
          });
        }
      });
    } else {
      console.log('최초로그인');
      req.session.user_name = isUser.user_name;
      req.session.user_id = isUser.user_id;

      return res.json({
        message: '로그인 성공',
        expiredTime: req.session.cookie.expires.getTime(),
      });
    }
  } catch (error) {
    console.error(error);
    res
      .status(500)
      .json({ error, success: false, message: '로그인 실패' });
  }
};

const findSessionId = async (id: string) => {
  const files = await fs.promises.readdir(SESSION_PATH);
  for (const file of files) {
    const cookie = await fs.promises.readFile(
      `${SESSION_PATH}/${file}`,
      'utf-8',
    );
    const sessionData = JSON.parse(cookie);
    if (sessionData.user_id == id) {
      return file;
    }
  }
  return null;
};

🎠 최초 로그인

  1. 회원 검증
    const { user_id, password } = req.body;

    const isUser = await User.findOne({ where: { user_id } });
    if (!isUser || !bcrypt.compareSync(password, isUser.user_password)) {
      return res.status(400).json({ message: '회원 정보가 존재하지 않습니다.' });
    }

req.body에 담겨온 user_idpassword를 통해 회원 검증을 한다.

  • DB에 입력한 user_id 정보가 존재하는지 확인한 후,
  • user_id가 존재하지 않거나 bcrypt 검증을 통해서 DB에 등록된 비밀번호와 입력한 password가 일치하지 않을 경우 실패 응답을 반환한다.

아, 분명 처음에 코드를 작성할 때 req.body에 그냥 id를 보내려고 하면 실패했었단 말임... 그래서 user_id를 통해서 받아왔는데 지금 혹시 몰라서 다시 해보니까 되는 것 같기도...? 왜인진 모르겠음... 아무튼 일단 넘어가고 나중에 다시 찾아보는 것으로...

  1. 회원 검증 이후 세션 정보 추가
	  req.session.user_name = isUser.user_name;
      req.session.user_id = isUser.user_id;

      return res.json({
        message: SUCCESS_MESSAGE.LOGIN,
        expiredTime: req.session.cookie.expires.getTime(),
      });

코드 자체는 간단하다. 세션 데이터에 회원의 이름과 아이디를 저장하여 식별할 수 있도록 만드는 것이다.

🎪 타입스크립트 SessionData 인터페이스 추가

🪄 express-session 파일 내의 SessionData 수정

하지만... 이 코드를 생성하기까지 상당히 오랜 시간이 걸렸다. typescript에서는 내가 세션 데이터에 user_name이나 user_id를 할당하고 싶다고 해서 바로 할당되는 것이 아니었다. 계속된 빨간줄에 한동안 이유를 못찾아서 미치고 팔짝 뛸 노릇 💦

그러던 중 여기 이 블로그에서 해답에 근접한 답을 찾을 수 있었다.

세션 데이터 인터페이스에 우리가 추가하고 싶은 세션 데이터를 정의해주어야만 코드에서 사용할 수가 있었던 것이다!

실제로 express-session 자체를 보니 209번째 줄에

세션 데이터 인터페이스가 있었다.

    interface SessionData {
        cookie: Cookie;
        user_name: string;
        user_id: string;
    }

여기에 이렇게 내가 추가하고 싶은 데이터를 정의해주면 무리없이 세션 데이터를 불러올 수 있게 된다.

로컬에서는 말이다.

위에 파일 경로를 잘 보면 session에 대한 정보값이 담긴 파일이 node_modules에 있다. 그렇다는 이야기는?! 배포된 사이트에서는 먹히지 않을 수도 있다는 거... 보통 node_modules 파일의 경우 용량이 커서 .gitignore를 통해 커밋에서 제외시키는 경우가 많기 때문이다.

🪄 우리가 정의하는 interfaces 파일에 SessionData 추가

그렇다면 우리가 정의해서 사용하는 interface에 SessionData를 추가하면 될 일이다.

index.d.ts(type definition파일)에서 세션 데이터를 정의하러 가본다.

import { SessionData } from 'express-session';

export interface ISessionUser extends SessionData {
  user_id: string;
  user_name: string;
}

express-session에서 SessionData를 불러온 후 새롭게 내가 ISessionUser를 정의했다.

이 방법을 통해 세션에 정보를 저장할 수 있다. 하지만!

      req.session['user_name'] = isUser.user_name;
      req.session['user_id'] = isUser.user_id;

왜인지 모르겠지만 이 방식으로 인터페이스를 정의할 경우에는 점 표기법(Dot Notation)을 사용할 수 없었다!

사실 나는 이것이 안 좋은 방식인지는 잘 모르겠다. 점 표기법과 대괄호 표기법의 차이는 이렇다. 대괄호 표기법을 사용할 경우 이후에 문제가 생기는지는 모르겠지만...? 아무튼 가독성이 떨어지잖아요? 때문에 점 표기법을 사용하고 싶다면 아래의 방법대로 하면 된다.

🪄 모듈 선언

express-session 파일의 예시를 보고 따라했다.

index.d.ts에 방금의 코드 대신 아래와 같이 선언해준다.

declare module 'express-session' {
  interface SessionData {
    user_id?: string;
    user_name?: string;
  }
}

이렇게 해주면!!!

	  req.session.user_name = isUser.user_name;
      req.session.user_id = isUser.user_id;

위와 같이 점 표기법으로 세션 데이터를 정의할 수 있다.

왜 이런 차이가 발생하는지 알아보면 좋을 것 같다. 나중에... 우선은 이 글을 마무리하는 것을 목표로 함.

글이 길어지니... 중복 로그인 방지에 대한 이야기는 다음 글에서 이어서 하도록 하겠다.


참고한 문서 및 블로그

도움이 될 것 같은 블로그

profile
영차영차 😎

0개의 댓글