23.05.06 웹개발_Backend (Cookie 의사코드)

Yeondong Choe·2023년 5월 6일
0

Cookie를 구현하기 위한 Client와 Server의 흐름을 확인하고 구현한 의사코드를 작성해보며 복습해보려고 한다.

[server_index.js / 흐름0: cors설정해주기, endpoint설정해주기]
const express = require('express');
const cors = require('cors');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const fs = require('fs');
const https = require('https');
const controllers = require('./controllers');
const app = express();

//mkcert에서 발급한 인증서를 사용하기 위한 코드입니다. 삭제하지 마세요!
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

const HTTPS_PORT = process.env.HTTPS_PORT || 4000;

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

//서버는 4000번, 클라이언트는 3000번을 사용하기에 포트번호가 다르기에 크로스 오리진이기에 cors설정을 해줘야 요청과 응답이 왔다갔다함
//흐름 0: cors 설정해주기
const corsOptions = {
  origin: "http://localhost:3000",
  credentials: true,
  //*cors요청은 option으로 주고 받음 따라서 option은 항상 넣어주기
  methods: ['GET', 'POST', 'OPTION']
};
app.use(cors(corsOptions));


//로컬 호스트 4000번대의 endpoint처리를 해주고 있음 
//요청에 맞게 실행시키는데 예를들어 login이라면 controllers의 login에 있는 export된 함수를 실행 
app.post('/login', controllers.login);
app.post('/logout', controllers.logout);
app.get('/userinfo', controllers.userInfo);

//endpoin에 cookie라고 했을때 Hello Cookie!가 나오는지 확인해보기
app.get('/cookie', (req, res) => res.send('Hello Cookie!'));
[client_Login.js / 흐름1: 로그인 버튼을 눌렀을때 서버로 로그인 요청하기]
import React, { useState } from 'react';
import axios from 'axios';

export default function Login( {setIsLogin, setUserInfo}) {
  //흐름1: 로그인을 위한 정보를 관리하는 상태관리로서 서버에 전달해줄 상태
  const [loginInfo, setLoginInfo] = useState({
    userId: '',
    password: '',
  });
  const [checkedKeepLogin, setCheckedKeepLogin] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const handleInputValue = (key) => (e) => {
    setLoginInfo({ ...loginInfo, [key]: e.target.value });
  };
  //흐름1: login button을 눌렀을때 이벤트함수가 실행되는것 
  const loginRequestHandler = () => {
    
    // TODO: Login 컴포넌트가 가지고 있는 state를 이용해 로그인을 구현합니다.
    // 로그인에 필요한 유저정보가 충분히 제공되지 않았다면 에러메시지가 나타나도록 구현하세요.
    //axios로 login을 담당하는 endpoint로 호스트요청 보내기
    //post의 두번째 인자로 필요한 데이터를 전달할 수 있음 
    return axios
      .post("http://localhost:4000/login", {loginInfo, checkedKeepLogin})
      .then((res) => {
        // 로그인에 성공했다면 응답으로 받은 데이터가 Mypage에 렌더링되도록 State를 변경하세요.
        //흐름3: 로그인 성공에 따라 props로 받아온 상태 변경해주기
        //흐름 2에서 보내준 userInfo가 password없이 잘 들어오는지 확인
        console.log(res.data)
        setUserInfo(res.data) //userInfo에는 받아온 값 넣어주기
        setIsLogin(true) //true로 바꿔주기
      })
      .catch((err) => {
        // 로그인에 실패했다면 그에 대한 에러 핸들링을 구현하세요. 
      });
    
  };

  return (
    <div className='container'>
      <div className='left-box'>
        <span>
          Education
          <p>for the</p>
          Real World
        </span>
      </div>
      <div className='right-box'>
        <h1>AUTH STATES</h1>
        <form onSubmit={(e) => e.preventDefault()}>
          <div className='input-field'>
            <span>ID</span>
            <input type='text' data-testid='id-input' onChange={handleInputValue('userId')} />
            <span>Password</span>
            <input
              type='password'
              data-testid='password-input'
              onChange={handleInputValue('password')}
            />
            <label className='checkbox-container'>
              <input type='checkbox' onChange={() => setCheckedKeepLogin(!checkedKeepLogin)} />
              {' 로그인 상태 유지하기'}
            </label>
          </div>
          <button type='submit' onClick={loginRequestHandler}>
            LOGIN
          </button>
          {errorMessage ? (
            <div id='alert-message' data-testid='alert-message'>
              {errorMessage}
            </div>
          ) : (
            ''
          )}
        </form>
      </div>
    </div>
  );
}
[server_login.js / 흐름2: 요청받은 정보가 일치한다면 유저정보와 쿠키를 Server_userinfo.js로 전달]
const { USER_DATA } = require('../../db/data');
//흐름 2
module.exports = (req, res) => {
  //client에서 보내준 정보를 잘 받아오고있는지 확인하기
  console.log(req.body)
  //login에 있어서 받아오고 있는 정보를 확인해보면 req.body를 통해 userId, password, checkedKeepLogin여부를 받아오고 있음 
  const { userId, password } = req.body.loginInfo;
  const { checkedKeepLogin } = req.body;
  //더미데이터로 관리하고있는 userdata를 filter해서 요청에서 받아온 userId와 password가 일치하는지 걸러내서 복사해서 저장하기  
  //더미데이터의 원본을 오염시킬 수 있기에 복사해서 사용하기
  const userInfo = {...USER_DATA.filter((user) => user.userId === userId && user.password === password)[0]};
  //콘솔로 확인해볼 수 있음.
  console.log(userInfo)
  //filter했을때 만약 userInfo.id가 없다면 401 상태의 권한없음을 보여주고 잘들어왔다면 쿠키를 전달해주기

  const cookieOptions = {
    //어떤 도메인에 저장할것인지 정해주기
    domain: 'localhost',
    path: '/',
    //https 프로토콜을 사용할때만 쿠키를 전달해준다는 옵션(localhost는 개발용이기에 안적어주는 경우도 많음, 탈취의 위험이 없기때문, 사용하게된다면 항상 true로 하기)
    secure: true,
    //쿠키는 consloe창에서 확인을 할 수 있는데 httpOnly 옵션을 true로 주면 console에서 쿠키 확인을 막을 수 있음 보안을 위해 항상 true로 주기
    httpOnly: true,
    //프로토콜과 포트가 가르고 사이트가 같은곳만 전달하는 옵션
    sameSite: 'strict'
    //expir, maxAge 설정이 없기때문에 브라우저를 닫으면 쿠키가 없어짐
  }

  if(userInfo.id === undefined) {
    res.status(401).send('Not Authorized')
  } 
  //옵션 설정할때 expire, maxAge 설정이 없기때문에 브라우저를 닫으면 쿠키가 없어짐 따라서 로그인상태 유지 체크가 되었다면 그때 설정해주기
  else if(checkedKeepLogin) {
    cookieOptions.maxAge = 1000 * 60 * 30 //단위가 ms 이기때문에 1초, 1분, 30분을 이렇게 작성함
    res.cookie('cookieId', userInfo.id, cookieOptions)
    res.redirect('/userinfo')
  }
  else {
    //res.cookie로 전달할 수 있고 인자로 (저장할이름, 어떤값을 저장, 저장할때 옵션) 저장해야함 
    res.cookie('cookieId', userInfo.id, cookieOptions)
    //send로 마무리 해줘야하지만 지금은 /useinfo로 리다이렉트 해줘야함
    res.redirect('/userinfo')
    //개발자도구 -> 애플리케이션 -> 쿠키에 저장되는것을 확인할 수 있음
  }
};

loginInfo, checkedKeepLogin 받아오고 있는지 콘솔에 확인

들어온 loginInfo 정보와 일치하는 userInfo가 있는지 콘솔에서 확인하기

정상적으로 들어왔다면 쿠키가 브라우저에 정상적으로 전송

[Server.js_userinfo.js / 흐름2: 로그인 정보에 맞는 쿠키를 전달받아 userInfo 전달하기]

const { USER_DATA } = require('../../db/data');

//흐름 2
module.exports = (req, res) => {
  //cookie가 잘들어오고있는지 확인해보기 확인된 쿠키로 유저를 찾아서 보내주기
  console.log(req.cookies)
  //USER_DATA에 filter를 사용해서 user.id와 받아온 쿠키의 cookieId가 같은거를 가져오기
  const userInfo = {...USER_DATA.filter((user) => user.id === req.cookies.cookieId)[0]};
    console.log(userInfo)
    //쿠키가 들어와서 회원 정보를 찾으러 갔는데 회원 정보가 없을때(회원탈퇴 등등) 로그인처리 시켜주면 안되고, 쿠키값이 제대로 들어오지 않았을때도 로그인되면 안됨
    if(!req.cookies.cookieId || !userInfo.id) {
      res.status(401).send('Not Authorized')
    } 
    // 그렇지 않다면 로그인 할 수 있는데 그때 userInfo 중 민감한 정보는 삭제해야함
    else {
      delete userInfo.password
      res.send(userInfo)
    }
};
[client_App.js / 흐름3: 로그인에 맞는 상태 전달, 흐름4: 로그인 조건에 따른 조건부 렌더링]
import './App.css';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './pages/Login';
import Mypage from './pages/Mypage';
import React, { useEffect, useState } from 'react';
import axios from 'axios';

// 모든 요청에 withCredentials가 true로 설정됩니다.
axios.defaults.withCredentials = true;

function App() {
  //흐름3: 받아온 userInfo에 맞게 상태를 변경해줘야하기에 porps로 내려주기
  const [isLogin, setIsLogin] = useState(false);
  const [userInfo, setUserInfo] = useState(null);

  return (
    <BrowserRouter>
      <div className='main'>
        <Routes>
          <Route
            path='/'
            element={
              //흐름4: 로그인이 된다면 Mypage를 로그인이 안된다면 Login페이지를
              isLogin ? ( 
                //흐름3: 로그인으로 컴포넌트에서 변경할 수 있도록 props를 전달
                <Mypage userInfo={userInfo}
                //흐름7: 로그아웃으로 컴포넌트에서 변경할 수 있도록 props를 전달
                setIsLogin={setIsLogin} setUserInfo={setUserInfo}/>
              ) : ( //흐름3: 로그인으로 컴포넌트에서 변경할 수 있도록 props를 전달
                <Login setIsLogin={setIsLogin} setUserInfo={setUserInfo}/>
              )
            }
          />
        </Routes>
      </div>
    </BrowserRouter>
  );
}
export default App;
[Client_Login.js / 흐름3: 로그인 응답 받아 상태 업데이트]
import React, { useState } from 'react';
import axios from 'axios';

export default function Login( {setIsLogin, setUserInfo}) {
  //흐름1: 로그인을 위한 정보를 관리하는 상태관리로서 서버에 전달해줄 상태
  const [loginInfo, setLoginInfo] = useState({
    userId: '',
    password: '',
  });
  const [checkedKeepLogin, setCheckedKeepLogin] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const handleInputValue = (key) => (e) => {
    setLoginInfo({ ...loginInfo, [key]: e.target.value });
  };
  //흐름1: login button을 눌렀을때 이벤트함수가 실행되는것 
  const loginRequestHandler = () => {
    
    // TODO: Login 컴포넌트가 가지고 있는 state를 이용해 로그인을 구현합니다.
    // 로그인에 필요한 유저정보가 충분히 제공되지 않았다면 에러메시지가 나타나도록 구현하세요.
    //axios로 login을 담당하는 endpoint로 호스트요청 보내기
    //post의 두번째 인자로 필요한 데이터를 전달할 수 있음 
    return axios
      .post("http://localhost:4000/login", {loginInfo, checkedKeepLogin})
      .then((res) => {
        // 로그인에 성공했다면 응답으로 받은 데이터가 Mypage에 렌더링되도록 State를 변경하세요.
        //흐름3: 로그인 성공에 따라 props로 받아온 상태 변경해주기
        //흐름 2에서 보내준 userInfo가 password없이 잘 들어오는지 확인
        console.log(res.data)
        setUserInfo(res.data) //userInfo에는 받아온 값 넣어주기
        setIsLogin(true) //true로 바꿔주기
      })
      .catch((err) => {
        // 로그인에 실패했다면 그에 대한 에러 핸들링을 구현하세요. 
      });
    };
[Client_Mypage.js / 흐름5: 로그아웃버튼 구현]
import axios from 'axios';
import React from 'react';

export default function Mypage({ userInfo, setIsLogin, setUserInfo }) {
  const logoutHandler = () => {
    //흐름5: 로그아웃 버튼을 구현 
    //axios로 login을 담당하는 endpoint로 호스트요청 보내기
    //post의 두번째 인자로 필요한 데이터를 전달할 수 있음 
    return axios
      .post("http://localhost:4000/logout")
      .then((res) => {
        // 로그아웃에 성공했다면 App의 상태를 변경하세요.
        //흐름7: 로그아웃 성공으로 쿠키를 삭제했다면 상태를 비워주기
        setIsLogin(false)
        setUserInfo(null)
      })
      .catch((err) => {
        // 로그아웃에 실패했다면 그에 대한 에러 핸들링을 구현하세요. 
      });
    
  };

  return (
    <div className='container'>
      <div className='left-box'>
        <span>
          {`${userInfo.name}(${userInfo.userId})`}님,
          <p>반갑습니다!</p>
        </span>
      </div>
      <div className='right-box'>
        <h1>AUTH STATES</h1>
        <div className='input-field'>
          <h3>내 정보</h3>
          <div className='userinfo-field'>
            <div>{`💻 ${userInfo.position}`}</div>
            <div>{`📩 ${userInfo.email}`}</div>
            <div>{`📍 ${userInfo.location}`}</div>
            <article>
              <h3>Bio</h3>
              <span>{userInfo.bio}</span>
            </article>
          </div>
          <button className='logout-btn' onClick={logoutHandler}>
            LOGOUT
          </button>
        </div>
      </div>
    </div>
  );
}
[Server_logout.js / 흐름6: 로그아웃처리하면서 쿠키 삭제하기]
module.exports = (req, res) => {
  //흐름6: 로그아웃한다면 저장해둔 쿠키를 삭제하기
  //res.clearCookie의 인자에는 어떤이름의 쿠키를 지울것인지, 만들때 사용했던 쿠키옵션을 정확하게 적어야함(하나라도 빠지면 안됨)
  //expire, maxAge는 예외할 수 있어서 없어도 됨
  const cookieOptions = {
    domain: 'localhost',
    path: '/',
    secure: true,
    httpOnly: true,
    sameSite: 'strict'
  }

  res.clearCookie('cookieId', cookieOptions).send('complete!')
};
[Client_Mypage.js / 흐름7: 로그아웃되면 상태 비우기]
import axios from 'axios';
import React from 'react';

export default function Mypage({ userInfo, setIsLogin, setUserInfo }) {
  const logoutHandler = () => {
    //흐름5: 로그아웃 버튼을 구현 
    //axios로 login을 담당하는 endpoint로 호스트요청 보내기
    //post의 두번째 인자로 필요한 데이터를 전달할 수 있음 
    return axios
      .post("http://localhost:4000/logout")
      .then((res) => {
        // 로그아웃에 성공했다면 App의 상태를 변경하세요.
        //흐름7: 로그아웃 성공으로 쿠키를 삭제했다면 상태를 비워주기
        setIsLogin(false)
        setUserInfo(null)
      })
      .catch((err) => {
        // 로그아웃에 실패했다면 그에 대한 에러 핸들링을 구현하세요. 
      });
  };
[Client_App.js / 흐름8: 조건부 렌더링으로 login상태 유지]
import './App.css';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './pages/Login';
import Mypage from './pages/Mypage';
import React, { useEffect, useState } from 'react';
import axios from 'axios';

// 모든 요청에 withCredentials가 true로 설정됩니다.
axios.defaults.withCredentials = true;

function App() {
  //흐름3: 받아온 userInfo에 맞게 상태를 변경해줘야하기에 porps로 내려주기
  const [isLogin, setIsLogin] = useState(false);
  const [userInfo, setUserInfo] = useState(null);

  const authHandler = () => {
    //흐름8: 초기 화면 렌더링시, 서버에 유저 정보를 요청하여 Login 또는 Mypage가 렌더링되도록 구현
    //user정보를 담당하는 endpint는 userinfo, 
    //login에서 유저정보를 보내주지않고 userinfo에서 쿠키값을 사용해서 유저 정보를 돌려보내주게 redirect시켰기때문

    return axios
      .get("http://localhost:4000/userinfo")
      .then((res) => {
        //userinfo endpoint로 요청을 보내고 userinfo가 처리해줘서 then값으로 들어옴
        //로그인이 될 수 있게 Login에서 해줬던 것을 그대로 가져오기
        setUserInfo(res.data) 
        setIsLogin(true)
      })
      .catch((err) => {
        // 인증에 실패했다면 그에 대한 에러 핸들링을 구현하세요. 
      });
  };

  useEffect(() => {
    // 흐름8: 처음 랜더링될때 실행되도록 useEffect를 사용 따라서 컴포넌트 생성 시 아래 authHandler() 함수가 실행됩니다.
    authHandler();
  }, []);

  return (
    <BrowserRouter>
      <div className='main'>
        <Routes>
          <Route
            path='/'
            element={
              //로그인이 된다면 Mypage를 로그인이 안된다면 Login페이지를
              isLogin ? ( 
                //흐름4: 로그인으로 컴포넌트에서 변경할 수 있도록 props를 전달
                <Mypage userInfo={userInfo}
                //흐름7: 로그아웃으로 컴포넌트에서 변경할 수 있도록 props를 전달
                setIsLogin={setIsLogin} setUserInfo={setUserInfo}/>
              ) : ( //흐름4: 로그인으로 컴포넌트에서 변경할 수 있도록 props를 전달
                <Login setIsLogin={setIsLogin} setUserInfo={setUserInfo}/>
              )
            }
          />
        </Routes>
      </div>
    </BrowserRouter>
  );
}
export default App;
profile
개발자 동동

0개의 댓글