로그인과 로그아웃

carrot·2021년 11월 22일
3

로그인과 로그아웃에 대하여

웹 애플리케이션을 만들면서 꼭 필요한 기능 중 하나는 회원가입과 로그인, 로그아웃 기능입니다. 이번시간에는 로그인과 로그아웃에 대한 로직을 정리해 보도록 하겠습니다.

불현듯 머릿속에 로그인과 로그아웃에 대한 생각이 떠올랐습니다. 부랴부랴 굿노트에 생각나는대로 정리했더니 나름(?) 괜찮게 정리가 되었습니다. 이 필기를 가지고 생각들을 정리해 보도록 하겠습니다.

1. Login

회원가입시 제출한 본인의 정보 중 ID(혹은 email), Password를 통해 본인 여부를 확인하는 과정 입니다. 또는 소셜 로그인 기능을 통한 확인도 있습니다. 소셜 로그인을 구현하는 경우 해당 소셜 로그인 기능을 제공하는 업체의 API를 따라서 적용하면 되기 때문에 구현 방법에 대해서는 서술하지 않겠습니다.

로그인 과정

로그인의 과정을 정리해 봅시다.

  1. Input에 입력된 ID 혹은 email 정보와 Password 정보를 포함하여 server에 request 요청을 보냅니다.
  2. server에서는 요청된 request body에 포함된 유저 정보(아이디와 패스워드)를 통해 DB를 검색하고 일치하는 유저 정보가 있는지 확인합니다.
  3. 일치 하는 정보가 있는 경우, 해당 유저 정보를 보내주고 token과 같은 인증 정보를 발행합니다.
  4. 일치 하는 정보가 없는 경우, 에러 메시지를 보냅니다.

request

로그인시 서버에 요청하는 requestPOST method를 사용합니다. 당연한 이야기지만 로그인 하려는 유저의 정보를 DB에서 확인하려면 유저가 보내는 ID, Password값이 필요하기 때문이죠. 이를 request.body에 담아 post 요청을 보내게 됩니다.

Axios.post("/user/login", requestBody)
	 .then((response) => {
		// 유저 데이터를 받아와서 활용..
	 })
	 .catch(e) {
  		// error handling
	 }

server

로그인 request를 받은 서버는 분주합니다. 할일이 많거든요. 로그인 request 요청을 받은 서버에서 하는 일을 정리해 봅시다.

  1. POST method를 통해 요청이 왔는지를 검증합니다.
  2. request body에 들어 있는 유저 정보가 유효한 값인지 검증합니다.
  3. DB를 검색하여 해당 유저의 정보를 확인합니다.
  4. 유저가 존재 하면 비밀번호를 검증합니다.
  5. 인증 결과를 증명할 토큰을 생성합니다.
  6. Cookie에 토큰값을 등록합니다.
  7. 유저 정보를 반환합니다.

위 과정에서 오류가 발생하면 그에 맞는 에러 코드와 메시지를 반환하도록 합니다. 자세히 정리해 보도록 하겠습니다.

1단계 method 검증이 끝나면 try catch를 통해 작업을 진행합니다. try catch를 사용하는 이유는 에러가 발생했을시 에러핸들링을 통해 서버를 죽이지 않기 위함입니다.

export default async(req, res) => {
	if(req.method === "POST") {
    	try {
        // 검증과정 진행
        } catch(e) {
        // 에러 핸들링
        }
    }
  	res.statusCode = 405;
  	return res.end();
}

2단계의 경우 주로 client에서 확인하여 해당 정보가 유효하지 않을시(아이디나 패스워드 값이 "" 이라던지 ..) request 요청을 안보내는 것이 가장 좋습니다.
하지만 여러가지의 이유로 만에 하나 유효하지 않은 값이 들어온 경우가 있을 수 있으므로 한번 더 검증하는 과정을 거치도록 합니다.

const { email, password } = req.body;
if( !email || !password ) {
	res.statusCode = 400;
  	return res.send("필수 데이터가 없습니다.")
}

필수 정보가 있다면 아이디를 통해 DB에 저장된 유저 정보를 찾습니다. 이 과정은 DB가 제공하는 API를 따라서 요청하면 되겠습니다.
해당 아이디를 가진 유저 정보가 없다면 res.statusCode = 404로 하여 해당 유저가 없음을 반환하면 되겠습니다.

해당 유저가 DB에 있으면 password를 검증합니다. 일반적으로 회원가입시 입력하는 비밀번호는 암호화 되어 저장됩니다. 주로 bcrypt와 같은 라이브러리를 사용하며 암호화, 복호화 api를 제공하므로 이러한 기능을 제공하는 라이브러리를 활용하시면 되겠습니다.
비밀번호가 일치하지 않는 경우 statusCode 403을 에러 메시지와 함께 반환하면 됩니다.

유저 검증이 끝나면 이를 증명할 수 있는 유효한 값의 token을 발행합니다. 이는 주로 Cookie header를 설정하는 방법으로 발행되며, httponly값을 주어 JS로 수정할 수 없도록 제공합니다. 또한 토큰과 함께 유효 기간을 함께 부여합니다.

token 생성에는 bcrypt와 마찬가지로 jsonwebtoken 라이브러리를 활용합니다.

response header를 설정하는 과정은 아래와 같습니다.

res.setHeader(
  "Set-Cookie",
  `access_token=${token}; path=/; expires=${new Date(Date.now() + 60 * 60 * 1000 * 24).toUTCString()}; httponly`
)

access_token이라는 이름으로 cookie를 지정하며 이때 expires값을 통해 유효 기간을 설정할 수 있습니다. 위 예시는 만 하루이며, 24 뒤에 원하는 일수를 곱해주면 원하는 일수를 유효기간으로 설정할 수 있습니다.

response header 설정이 끝났다면 이제 DB에서 찾은 유저의 정보를 반환합니다.
이때 password와 같이 client단에서 불필요한 정보들은 미리 제거하고 반환할 수 있도록 주의 합니다.

Logout

로그아웃을 구현하고 실행하는 방법에는 여러 종류가 있겠지만 주로 로그인시 발급받은 토큰의 유효기간을 만료하는 방법을 사용합니다. 토큰값을 만료하면서 클라이언트에 저장된 유저 정보를 초기화 시킵니다. redux를 사용한다면 store에 저장된 유저 정보를 초기화 시키는 dispatch를 보내는 것이죠. 이를 구독하고 있는 메인 페이지는 유저 정보가 없으므로 로그인 전의 화면을 출력하는 구조가 되어야 겠습니다.

로그아웃 과정

  1. DELETE method를 통해 request 요청을 보냅니다.
  2. 서버에서는 response header의 쿠키값을 초기화하고 인증시간을 만료합니다.
  3. 서버의 response가 정상적으로 종료되면 client에서는 상태를 관리하는 값을 변경하여 유저 정보를 초기화 하고 이에 따른 로그인 화면을 렌더링 합니다.

request

로그아웃시 delete method를 통해 request를 보냅니다. 여기서 설명하는 구현 방법에서는 유저의 토큰값을 만료하는 방식을 사용하므로 유저의 아이디와 같은 정보를 보내지 않고 처리할 수 있습니다.

Axios.delete("/user/logout")

server

로그인시와 마찬가지로 우선 method 유효성을 검사합니다.

export default async(req, res) => {
	if(req.method === "DELETE") {
    	try {
        	// 로그아웃 절차 진행
        } catch(e) {
        	// 에러 핸들링
        }
    }
  
  	// method가 delete가 아닌 경우
  	res.statusCode = 405;
  	return res.end();
}

cookie값을 재설정 합니다. 토큰 값을 초기화 하고 유효기간을 만료 하도록 합니다.

      res.setHeader(
        "Set-Cookie",
        "access_token=; path=/; expires=Thu, Jan 1970 00:00:00 GMT; httponly"
      );
      res.statusCode = 204;
      return res.end();

client

서버의 응답이 정상적으로 종료되면 클라이언트에서는 이후 redux와 같은 상태관리 라이브러리의 상태값을 변경하여 이를 구독하고 있는 화면의 렌더링을 변경하는 방법으로 로그아웃을 마무리합니다.

주로 유저의 로그인 상태를 표현하는 Header 컴포넌트에서 useSelector를 통해 user의 로그인 값을 구독하고 이 값의 변경 여부를 통해 다른 메뉴를 출력(로그인/회원가입 또는 로그인된 유저 정보)하는 방식을 사용합니다.


const isLogged = useSelector((state) => state.user.isLogged);
	...
      {!isLogged && <HeaderAuths />}
      {isLogged && <HeaderUserProfile />}
   	...

정리

  1. api는 post, delete를 사용합니다.
  2. 로그인 데이터, DB검증, Password검증, 토큰발행, header설정, 유저값 반환 등의 과정을 정의합니다.
  3. 에러가 발생할 시 상황에 맞는 statusCode를 설정하고 에러 메시지를 반환합니다.
  4. client에서는 response 결과에 따라 redux와 같은 상태관리 라이브러리의 상태값을 변경하여 이를 구독하고 있는 컴포넌트의 리렌더링을 유도하여 로그인/아웃을 구현합니다.

회원가입이나 로그인, 로그아웃과 같은 기능을 막상 구현하려고 하면 내용이 잘 정리되지 않아서 구현하는 중간중간 코드를 변경하는 등의 시행착오를 거치게 됩니다. 물론 구현 방식에 더 좋은 코드는 있지만 정답은 없기에 다양한 방법이 존재할 것입니다.

웹 개발자의 경우 이러한 기능이 대부분 포함되는 애플리케이션을 개발하게 되기 때문에 한 번쯤은 본인의 방식을 잘 정리해 두면 좋을것 같습니다.

profile
당근같은사람

8개의 댓글

comment-user-thumbnail
2022년 2월 11일

comment 1

2개의 답글