• 카테고리

    질문 & 답변
  • 세부 분야

    풀스택

  • 해결 여부

    해결됨

새로 고침 시 로그인이 유지되지 않는 버그가 발생합니다!(쿠키 존재O)

24.02.08 02:37 작성 24.02.08 03:27 수정 조회수 362

1

안녕하세요 제로초님!
AWS에 배포하기 및 카카오톡 공유하기 강의까지 끝마친 수강생 입니다!
저는 윈도우 10, 크롬 브라우저, VSCode, MySQL 8버전을 사용하고 있습니다.

로그인 후 새로 고침을 하면 로그인이 유지되지 않고 풀려 버리는 버그가 발생하여
질문을 올리게 되었습니다! 이 문제는 쿠키가 있음에도 발생합니다!


이 버그로 인해 총 2가지 상태에 이릅니다.
1. 완전히 로그인을 한 상태에서 새로 고침을 하면 쿠키는 남아있으나 로그인이 유지되지 않습니다.
2. 완전히 로그인이 된 상태에서 프로필 페이지로 이동하면 로그인이 풀리며 홈페이지로 돌아옵니다.


새로 고침 후 로그인이 풀리는 문제가 발생한 시점에서
다시 로그인을 하려고 하면 로그인 유무 검사(로그인하지 않은 사용자만 접근 가능) 메시지가 뜹니다.
로그인 후 로그아웃을 하면 해당 문제가 발생하지 않습니다.

로그인문제1.PNG

해당 에러를 임시로 해결하는 방법을 찾았습니다.
프론트 페이지와 백엔드 페이지 애플리케이션 탭에서 쿠키를 지우면 다시 로그인을 진행할 수 있습니다.
하지만 말 그대로 임시로 해결하는 것 뿐이라 로그인 후 새로 고침을 하면
아까와 똑같이 로그인이 풀려버립니다.
문제를 해결하기 위해 페이지에서 콘솔 탭과 네트워크 탭, 애플리케이션 탭의 쿠키 쪽을 확인하였습니다.


프론트 콘솔 탭 확인
해당 에러 주소로 들어가 보았지만 외계어로 적혀있어 다른 곳을 먼저 확인해보기로 했습니다.
로그인문제2_프론트로그인콘솔.PNG

프론트 네트워크 탭 확인
401 Unauthorized 에러를 네트워크 탭에서 확인 했습니다.
Headers 탭에 적힌 쿠키와 Cookies 탭에 적힌 쿠키는 똑같았습니다!
로그인문제2_프론트로그인네트워크.PNG

프론트 쿠키 존재 확인
로그인 후 로그아웃을 진행하면 쿠키가 남아있습니다.
로그인문제3_프론트쿠키.PNG
백엔드 쿠키 존재 확인
백엔드 페이지도 프론트 쪽과 똑같이 쿠키가 잘 들어가 있음을 확인하였습니다.
로그인문제4_백엔드쿠키.PNG

페이지 쪽을 전부 확인한 뒤, 우분투 백엔드 pm2 monit을 확인해보니
사용자 로그인 쪽에서 401 에러가 나고 있었습니다.

로그인문제4_우분투백엔드1.PNG

sudo npx pm2 logs --err --lines 200 명령어로 에러 로그를 확인하였으나
관련 에러는 찾지 못하였습니다.
문제를 해결하기 위해 잘못 적은 코드가 있는지 검사하였습니다.
아래는 가장 의심되는 코드들 입니다!
리듀서, 사가, 라우터, 로그인 검사, 서버사이드 렌더링 순으로 작성하였습니다.

front/reducers/user.js
나의 사용자 정보 불러오기 리듀서

const reducer = (state = initialState, action) => {
  return produce(state, (draft) => {
    switch (action.type) {

      /* 나의 사용자 정보 불러오기 요청 리듀서 */
      case LOAD_MY_INFO_REQUEST:
        draft.loadMyInfoLoading = true;
        draft.loadMyInfoError = null;
        draft.loadMyInfoDone = false;
        break;
      /* 나의 사용자 정보 불러오기 성공 리듀서 */
      case LOAD_MY_INFO_SUCCESS:
        draft.loadMyInfoLoading = false;
        draft.me = action.data;
        draft.loadMyInfoDone = true;
        break;
      /*  나의 사용자 정보 불러오기 실패 리듀서 */
      case LOAD_MY_INFO_FAILURE:
        draft.loadMyInfoLoading = false;
        draft.loadMyInfoError = action.error;
        break;


로그인 리듀서

. . .

      /* 로그인 요청 리듀서 */
      case LOG_IN_REQUEST:
        draft.logInLoading = true;
        draft.logInError = null;
        draft.logInDone = false;
        break;
      /* 로그인 성공 리듀서 */
      case LOG_IN_SUCCESS:
        draft.logInLoading = false;
        draft.me = action.data; // 로그인 성공 시 실제 사용자 데이터
        draft.logInDone = true;
        break;
      /* 로그인 실패 리듀서 */
      case LOG_IN_FAILURE:
        draft.logInLoading = false;
        draft.logInError = action.error;
        break;

 

front/sagas/user.js
나의 사용자 정보 불러오기 사가

// loadMyInfo 실행 시 서버에 loadMyInfoAPI 요청
function loadMyInfoAPI() {
  return axios.get('/user');
}
// LOAD_MY_INFO 액션이 실행되면 loadMyInfo 함수 실행
function* loadMyInfo(action) {
  /* 요청 성공 시 LOAD_MY_INFO_SUCCESS 액션 디스패치 */
  try {
    const result = yield call(loadMyInfoAPI, action.data);
    yield put({
      type: LOAD_MY_INFO_SUCCESS,
      data: result.data,
    });

  /* 요청 실패 시 LOAD_MY_INFO_FAILURE 액션 디스패치 */
  } catch (err) {
    console.error(err);
    yield put({
      type: LOAD_MY_INFO_FAILURE,
      error: err.response.data, // 실패 결과
    });
  }
}


로그인 사가

// logIn 실행 시 서버에 logInAPI 요청
function logInAPI(data) {
  return axios.post('/user/login', data);
}
// LOG_IN_REQUEST 액션이 실행되면 logIn 함수 실행
function* logIn(action) {
  /* 요청 성공 시 LOG_IN_SUCCESS 액션 디스패치 */
  try {
    const result = yield call(logInAPI, action.data);
    yield put({
      type: LOG_IN_SUCCESS,
      data: result.data,         // 성공 결과 : 서버로부터 사용자 정보를 받아옴
    });

  /* 요청 실패 시 LOG_IN_FAILURE 액션 디스패치 */
  } catch (err) {
    console.error(err);
    yield put({
      type: LOG_IN_FAILURE,
      error: err.response.data, // 실패 결과
    });
  }
}


back/routes/user.js
브라우저 새로 고침 시 나의 사용자 정보를 복구하는 라우터
나의 사용자 정보를 복구하는 라우터는 user 라우터들 중에서 제일 위에 위치합니다!

// 브라우저 새로고침 시 나의 사용자 정보를 복구하는 라우터
router.get('/', async (req, res, next) => { // GET /user
  // req.headers 안에 쿠키가 들어있다.
  console.log(req.headers, "req.headers 안에는 쿠키가 들어있다.");
  try {
    /* (로그인해서) 사용자 정보가 있다면 */
    if (req.user) {
      /* (비밀번호를 제외한) 모든 사용자 정보를 가져오는 함수 */
      const fullUserWithoutPassword = await User.findOne({
        where: { id: req.user.id },
        attributes: { exclude: ['password'] },
        // 모델 가져오기
        include: [{
          /* 나의 게시글 */
          model: Post,
          attributes: ['id'], // id 데이터만 가져오기
        }, {
          /* 나의 팔로잉 */
          model: User,
          as: 'Followings',
          attributes: ['id'],
        }, {
          /* 나의 팔로워 */
          model: User,
          as: 'Followers',
          attributes: ['id'],
        }]
      });
      // 200번대 에러 출력
      res.status(200).json(fullUserWithoutPassword);
      
    /* (로그아웃해서) 사용자 정보가 없다면 */
    } else {
      // 아무것도 보내지 않기
      res.status(200).json(null);
    }

  /* 에러 캐치 */
  } catch (error) {
    console.error(error);
    next(error);
  }
});


로그인 라우터

// 로그인 라우터 : 사용자 로그인 전략 실행
router.post('/login', isNotLoggedIn, (req, res, next) => {
  /* '로컬', (서버 에러, 성공 객체, 클라이언트 에러)가 전달 */
  passport.authenticate('local', (err, user, info) => {
    // done에서 넣은 값들이 순서대로 전달되는 곳
    /* 서버 에러 */
    if (err) {
      console.error(err); // 콘솔 창을 통한 에러 메시지 출력
      return next(err);   // 에러 처리 미들웨어로 이동
    }
    /* 클라이언트 에러 : 로그인하다 에러가 나면 클라이언트로 응답 보내기 */
    if (info) {
      return res.status(401).send(info.reason);
    }
    /* 로그인 성공 객체 */
    return req.login(user, async (loginErr) => {
      // 서비스 로그인이 끝난 후 패스포트 로그인 할 때 에러발생 시 처리
      if (loginErr) {
        console.error(loginErr);
        return next(loginErr);
      }

      /* (비밀번호를 제외한) 모든 사용자 정보를 가져오는 함수 */
      const fullUserWithoutPassword = await User.findOne({
        where: { id: user.id },
        attributes: { exclude: ['password'] }, // 전체 데이터에서 비밀번호만 제외
        // 모델 가져오기
        include: [{
          /* 나의 게시글 */
          model: Post,
          attributes: ['id'], // id 데이터만 가져오기
        }, {
          /* 나의 팔로잉 */
          model: User,
          as: 'Followings',
          attributes: ['id'],
        }, {
          /* 나의 팔로워 */
          model: User,
          as: 'Followers',
          attributes: ['id'],
        }]
      });
      /* (비밀번호를 제외한) 모든 사용자 정보를 프론트로 넘기기 */
      return res.status(200).json(fullUserWithoutPassword);
    });
  })(req, res, next); // 미들웨어 커스터마이징
});


back/routes/middlewares.js
로그인 검사

// (로그인 안했을 때) 로그인 유무 검사
exports.isNotLoggedIn = (req, res, next) => {
  if (!req.isAuthenticated()) {
    next();
  } else {
    res.status(401).send('로그인하지 않은 사용자만 접근 가능합니다.');
  }
};

 

문제 해결을 위해 노드버드 커뮤니티에서 저와 비슷한 에러가 발생한 수강생 분이 계셨습니다!
https://www.inflearn.com/questions/368573/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%A7%88%EB%AC%B8-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%80-%EC%82%AC%EC%9A%A9%EC%9E%90%EB%A7%8C-%EC%A0%91%EA%B7%BC%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%A9%EB%8B%88%EB%8B%A4

해당 글에서 제로초님은 쿠키가 있는 걸로 봐서 이미 로그인이 된 상태인데
프론트에서 자신이 로그인 되었다는 것을 인식하지 못하고 있으며,
해당 getServerSideProps 쪽에 문제가 있다고 힌트를 주셨습니다!


제로초님의 노드버드Ch7 user/[id].js
https://github.com/ZeroCho/react-nodebird/blob/master/ch7/front/pages/user/[id].js


front/pages/user/[id].js
강의를 진행하며 깃허브와 약간은 다른 코드가 있습니다.

// 서버사이드 렌더링(SSR) : getServerSideProps 사용
/* 사용자 컴포넌트보다 먼저 실행, 매개변수 context 안에 store가 들어있다. */
export const getServerSideProps = wrapper.getServerSideProps(async (context) => {

  /* 변수 cookie에 모든 cookie 정보 저장 */
  const cookie = context.req ? context.req.headers.cookie : '';

  /* 쿠키를 안 써서 요청 보낼 때는 서버에서 공유하고 있는 쿠키를 제거하기 */
  axios.defaults.headers.Cookie = '';

  /* 서버일 때, 그리고 쿠키가 있을 때만 서버로 쿠키 전달하기 */
  if (context.req && cookie) {
    // 실제로 쿠키를 써서 요청을 보낼 때만 잠깐 쿠키를 넣어 놓는다.
    axios.defaults.headers.Cookie = cookie;
  }

  /* 처음에 화면을 로딩하면 특정 사용자의 게시글 불러오기 요청 액션 객체 디스패치 */
  context.store.dispatch({
    type: LOAD_USER_POSTS_REQUEST,
    data: context.params.id, // 또는 context.query.id로 useRouter에 접근 가능
  });

  /* 처음에 화면을 로딩하면 나의 사용자 정보 불러오기 요청 액션 객체 디스패치 */
  context.store.dispatch({
    type: LOAD_MY_INFO_REQUEST,
  });

  /* 처음에 화면을 로딩하면 다른 사용자 정보 불러오기 요청 액션 객체 디스패치 */
  context.store.dispatch({
    type: LOAD_USER_REQUEST,
    data: context.params.id, // 또는 context.query.id로 useRouter에 접근 가능
  });


  /* 나의 사용자 정보, 다른 사용자 정보 불러오기, 여러 게시글 불러오기
     요청(REQUEST)이 성공(SUCCESS)으로 바뀔 때까지 기다리기 */
  context.store.dispatch(END);
  await context.store.sagaTask.toPromise();
  console.log('getState', context.store.getState().post.mainPosts);
  return { props: {} };
});


리듀서, 사가, 그리고 해당 서버사이드 렌더링 쪽을 점검해보았으나 잘못된 점을 찾지 못하여 질문 남깁니다..
어떻게 하면 사용자(나)의 로그인이 유지되게 할 수 있을까요? 혹시 힌트 키워드라도 주실 수 있을까요?
긴 질문 글 읽어주셔서 감사합니다 제로초님 항상 강의 잘 보고 있습니다!

답변 3

·

답변을 작성해보세요.

1

이가은님의 프로필

이가은

질문자

2024.02.08

제로초님의 백엔드 NODE_ENV가 production이 아닐 수 있다는 말씀에서 힌트를 얻어 버그를 해결하였습니다!
백엔드 package.json의 start 스크립트에 cross-env NODE_ENV=production
코드를 적지 않아 새로 고침 시 로그인이 풀리는 문제가 발생했던 것입니다!
긴 질문 글임에도 끝까지 도와주셔서 감사합니다 제로초님! 행복한 설날 보내세요!

// 수정 전 백엔드 start 스크립트
"start": "pm2 start app.js"

// 수정 후 백엔드 start 스크립트
"start": "cross-env NODE_ENV=production pm2 start app.js"

1

app.js에서 express-session 설정 잘못하셨을겁니다.

이가은님의 프로필

이가은

질문자

2024.02.08

참고한 깃허브
https://github.com/ZeroCho/react-nodebird/blob/master/ch7/back/app.js

노드버드 세션 부분 강의와 제로초님의 노드버드 Ch7 깃허브와 비교하며
express-session 설정 부분 코드를 확인하였습니다.
그러나 session 부분에서 틀린 점을 찾지 못하였습니다!

// Express-Session 모듈 호출
const session = require('express-session');

. . .

// 쿠키와 세션 미들웨어 연결
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
  saveUninitialized: false,
  resave: false,
  secret: process.env.COOKIE_SECRET,
  /* ---------- 쿠키 옵션(Cookie Option) ---------- */
  cookie: {
    httpOnly: true, // true로 설정 시 JS로 쿠키에 접근하지 못하도록 막는다.
    secure: false,  // https 적용 시 true로 설정할 예정
    /* 쿠키의 도메인이 배포 환경이면 도메인 앞에 '.'을 붙이기 */
    // 점을 붙이면 api.nodebird.xyz와 nodebird.xyz 사이에서 쿠키 공유가 된다.
    domain: process.env.NODE_ENV === 'production' && '.nodebird.xyz'
  },
}));
app.use(passport.initialize());
app.use(passport.session());


딱 하나 다른 부분이 있다면 저의 경우 back/app.js에서 app.use(helmet()) 부분이 비어있으나
제로초님 깃허브에는 contentSecurityPolicy: false 값이 들어있습니다.

현재 노드버드 섹션 7까지는 나가지 않아서 https를 적용하지 않은 상태입니다.
아래 질문 글처럼 네트워크 쪽에 노란색 느낌표 경고 표시가 나타나진 않습니다.
https://www.inflearn.com/questions/450426/cookie-%EA%B3%B5%EC%9C%A0-%EC%A7%88%EB%AC%B8%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4
image

지금 백엔드쪽 application -> cookie 탭 보시면 쿠키의 도메인이 api.nodebird.xyz로 되어있는데 이러면 nodebird.xyz에서는 사용이 불가능합니다. 백엔드 NODE_ENV가 production이 아닌것 같은데요.

0

인프런 AI 인턴님의 프로필

인프런 AI 인턴

2024.02.08

안녕하세요, 인프런 AI 인턴입니다.

이가은님의 질문은 로그인 상태가 새로 고침 시 유지되지 않는 문제에 대한 것으로 보여집니다. 이와 관련하여 쿠키 관리 및 서버사이드 렌더링에 대한 이해가 필요합니다. 다음은 기존 답변 링크 중에서 유사한 문제에 대한 해결 방안을 제공하는 것으로 보이는 몇 가지입니다:

  • 쿠키 및 서버사이드렌더링 관련 문의드립니다.
    해당 문의에서는 로그아웃 후 다시 로그인 시 쿠키 관련 문제로 로그인 상태가 유지되지 않는 문제가 제기되었고, 질문에 대해 조현영 지식공유자님의 도움을 받아 문제를 해결하려 한 경우입니다.

  • 카카오 로그인 적용 관련해서 질문 드릴게요
    이 경우도 로그인 후 새로 고침하거나 페이지를 이동할 때 로그인이 풀리는 문제가 있었으며, 여기에서는 쿠키 전달 및 배포 시 발생하는 문제에 대한 논의가 있었습니다.

  • cookie 공유 질문있습니다.
    로그인 이후 새로 고침 했을 때 로그인 상태가 유지되지 않는 문제로, 서버 설정에서 HTTPS를 사용하기 위한 Secure 설정이 누락되었을 때 발생할 수 있는 문제에 대한 토론입니다.

  • 쿠키관련) 로그아웃이 되지 않음
    쿠키 문제로 로그아웃이 진행되지 않는 경우에 대한 질문과 지식공유자님의 답변이 있으며, 쿠키 관리에 대한 내용을 다루고 있습니다.

해당 답변들은 쿠키와 인증 관련 문제들을 다루고 있으며, 질문하신 내용과 관련하여 비슷한 맥락의 문제들에 대해 해결 방안을 제시하고 있습니다. 자세한 내용은 위 링크들을 참조해 주시기 바랍니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.