✏️  Front Lab's Tech Blog
thumbnail
로그인 후 창을 닫아도 로그인 유지 문제
All
2024.06.17.

프로젝트를 진행하던 중, 사용자가 브라우저 창을 닫아도 로그인이 유지되는 문제가 발생했습니다. 그리고 숨겨진 페이지의 URL을 직접 입력해서 이동할 경우 로그아웃되는 문제도 발견했습니다. 이 문제를 해결하기 위해 beforeunload 이벤트, 로컬스토리지, 세션스토리지를 사용해가며 해결했습니다.

1. 첫 번째 시도: beforeunload 사용

 beforeunload 이벤트는 사용자가 창을 닫으려 할 때 “저장되지 않은 내용이 있습니다. 정말 나가시겠습니까?” 와 같은 경고창을 띄울 때 자주 사용되는 함수입니다.
저는 이 함수를 사용해 브라우저 창이 닫힐 때 자동으로 로그아웃되도록 하려고 했습니다. 아래는 beforeunload를 사용한 코드입니다.

useEffect(() => {
  window.addEventListener('beforeunload', ev => {
    ev.preventDefault()
    return (ev.returnValue = 'Are you sure you want to close?')
  })

  return () => {
    const userSavedData = localStorage.getItem('userData')
    if (userSavedData !== null) {
      window.removeEventListener('beforeunload', auth.logout())
    }
  }
}, [])

 처음에는 창을 닫거나 페이지를 이동할 때 경고를 띄워주는 기능을 통해, 창이 닫힐 때 로그아웃이 되도록 설정하면 문제를 해결할 수 있을 것이라고 생각했습니다.
그러나 beforeunload는 창닫기, 페이지 이동, 새로고침의 경우를 구분하지 못하기 때문에, 모든 브라우저 상태 변화에서 로그아웃이 발생하는 문제가 있었습니다.

2. 두 번째 시도: beforeunload에 조건문 추가하여 브라우저 상태 구분하기

useEffect(() => {
  const handleBeforeUnload = event => {
    const navEntries = performance.getEntriesByType('navigation')
    const navEntry = navEntries.length > 0 ? navEntries[0] : null

    if (document.hasFocus() && navEntry && navEntry.type !== 'reload') {
      handleLogout()
      event.preventDefault()
      event.returnValue = '' // 일부 브라우저에서 필요
    }
  }

  window.addEventListener('beforeunload', handleBeforeUnload)

  const handleUnload = () => {
    // 창이 닫힐 때만 로그아웃 처리
    handleLogout()
  }

  window.addEventListener('unload', handleUnload)

  return () => {
    window.removeEventListener('beforeunload', handleBeforeUnload)
    window.removeEventListener('unload', handleUnload)
  }
}, [])

 이 코드는 beforeunload와 unload 이벤트를 사용해 창을 닫을 때와 새로고침할 때 각각 구분하여 처리했습니다. 하지만 창닫기, 페이지 이동, 새로고침의 세 가지 경우를 정확히 구분하지 못해 문제는 해결되지 않았습니다.

3. 세 번째 시도: 세션 스토리지를 활용한 해결책

 문뜩 로그아웃이 안되는건 로컬에 로그인에 대한 정보가 저장이 되어있어서 그런게 아닐까라는 생각이 들었고, 실제로 프로젝트에서 로컬 스토리지를 사용하고 있다는 점을 확인했습니다. 그래서 세션 스토리지를 활용하여 창이 닫혔을 때 자동으로 로그아웃되도록 시도했습니다.
🔗 [참고] (https://ellajang.github.io/basicResource/storage/)

📍 로컬 스토리지와 세션 스토리지의 비교

로컬 스토리지

  • 창을 닫아도 데이터가 남아있습니다.
  • 새로고침을 해도 데이터가 유지됩니다.
  • 다른 페이지로 이동해도 데이터가 유지됩니다.

세션 스토리지

  • 창을 닫으면 데이터가 초기화됩니다.
  • 새로고침을 해도 데이터가 유지됩니다.
  • 다른 페이지로 이동해도 데이터가 유지됩니다.

 따라서 세션 스토리지에 로그인 상태를 저장하면 창을 닫았을 때 자동으로 로그아웃될 수 있다고 판단했습니다.



Redux 상태를 세션 스토리지에 저장하기

 프로젝트에서 redux로 상태를 관리하고 있기 때문에 redux-persist 라이브러리를 사용해 리덕스 상태를 세션 스토리지에 저장하도록 설정했습니다.

  yarn add redux-persist

  다음은 store 관리 폴더에서 persist 설정을 적용한 코드입니다.

import { combineReducers, configureStore } from '@reduxjs/toolkit'
import storageSession from 'redux-persist/lib/storage/session'

const rootReducer = combineReducers({
  auth,
  user,
})

const persistConfig = {
  key: 'root',
  storage: storageSession,
  // blacklist: [""]  // 유지할 필요 없는 상태를 제외
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

export const store = configureStore({
  reducer: persistedReducer,
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
})

export const persistor = persistStore(store)

 그리고, _app.js 파일에 PersistGate를 추가해 Redux 상태를 세션 스토리지에 저장하도록 설정했습니다.

const { persistor, store } = getStore()

ReactDOM.render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <React.StrictMode>
        <ThemeProvider theme={theme}>
          <App />
          <ToastContainer />
        </ThemeProvider>
      </React.StrictMode>
    </PersistGate>
  </Provider>,
  document.getElementById('root'),
)

 마지막으로, 기존에 로컬 스토리지에 저장되어 있던 데이터를 세션 스토리지로 변경했습니다.

const userData = sessionStorage.getItem(authConfig.userData) //localStorage에서 sessionStorage로 변경

위와 같은 방법으로 해결했습니다.

📍 추가로 중요한점
 세션 스토리지를 활용할 경우, 서버의 최신 데이터를 동기화하기 위한 추가 작업이 필요합니다. redux-persist 는 로컬에 있는 데이터를 우선적으로 보여주기 때문에, 서버에서 최근의 데이터를 불러와야 하는 곳이라면 유의해서 비동기 액션을 추가하여 새로고침 시 서버로부터 데이터를 불러오는 것이 중요합니다.

4. 마무리

 이 문제는 단순히 브라우저 창을 닫는 것을 감지하는 것만으로 해결되지 않았습니다.
로컬 스토리지 대신 세션 스토리지를 사용하고, Redux 상태를 세션 스토리지에 저장하여 창을 닫았을 때 로그아웃을 처리했습니다. 추가로 서버와의 데이터 동기화를 통해 새로고침 후에도 최신 데이터를 유지하도록 설정함으로써 최종적으로 문제를 해결할 수 있었습니다.