← 전체 글로 돌아가기

웹 개발

로그인 상태가 자꾸 풀릴 때

React 앱에서 세션이나 토큰이 유실되는 문제를 진단하는 순서입니다.

로그인했는데 페이지를 새로고침하면 로그인이 풀려있다. 클릭해서 다른 페이지로 이동했을 때는 괜찮은데, 주소창에서 직접 URL을 치면 로그인이 풀려있다.

먼저 localStorage와 sessionStorage 확인

React 앱에서는 보통 토큰을 로컬 저장소에 저장한다.

// 로그인
localStorage.setItem('token', token);

// 페이지 로드 시
const token = localStorage.getItem('token');

로컬 저장소는 페이지를 새로고침해도 유지되어야 한다. 개발자 도구 (F12)에서 확인해보자.

// 브라우저 콘솔에서
localStorage
localStorage.getItem('token')

토큰이 있으면 저장 자체는 문제없다. 없으면 저장 실패 또는 삭제된 것이다.

토큰 저장/로드 로직 확인

loginHandler와 앱 초기화 로직을 확인한다.

// 로그인 후 저장
const handleLogin = async (credentials) => {
  const response = await login(credentials);
  const { token } = response.data;
  localStorage.setItem('token', token);
  setUser(response.data.user);
};

// 페이지 로드 시 복원
useEffect(() => {
  const token = localStorage.getItem('token');
  if (token) {
    // API 호출로 사용자 정보 검증
    fetchUserProfile(token);
  }
}, []);

이 두 부분이 제대로 연동되지 않으면, 새로고침 후 토큰을 읽어도 사용자 정보가 없다.

쿠키 vs 로컬 저장소

일부 앱은 쿠키를 사용한다. 쿠키는 도메인과 경로 제한이 있다.

// 로그인
res.cookies.set('token', token, {
  httpOnly: true,      // 자바스크립트에서 접근 불가
  secure: true,        // HTTPS만
  sameSite: 'strict',  // CSRF 방지
  maxAge: 7 * 24 * 60 * 60 * 1000 // 7일
});

만약 httpOnly: true라면, JavaScript에서는 쿠키를 읽을 수 없다. 대신 서버에서 자동으로 요청에 포함된다.

로컬 저장소 vs 쿠키:

  • 로컬 저장소: 자바스크립트로 조작 가능, XSS 취약점
  • 쿠키 (httpOnly): 자동 전송, XSS 방지

CORS와 크레덴셜 설정

API 요청이 다른 도메인으로 가면, 쿠키가 자동 전송되지 않는다.

// 클라이언트
fetch('https://api.example.com/user', {
  credentials: 'include' // 쿠키 포함
});

// 또는 axios
axios.defaults.withCredentials = true;

서버도 CORS 헤더를 제대로 설정해야 한다.

res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Origin', 'https://client.example.com');

토큰 만료 시간 확인

토큰이 저장되어 있지만, 만료되었을 수 있다.

const decodeToken = (token) => {
  try {
    const payload = token.split('.')[1];
    const decoded = JSON.parse(atob(payload));
    return decoded;
  } catch (e) {
    return null;
  }
};

const token = localStorage.getItem('token');
if (token) {
  const decoded = decodeToken(token);
  console.log('Expires:', new Date(decoded.exp * 1000));
  console.log('Now:', new Date());
}

만약 exp < now라면, 토큰이 이미 만료되었다.

새로고침과 마운트 순서

React에서 컴포넌트가 마운트될 때 토큰을 읽는데, 읽기 전에 렌더링이 시작될 수 있다.

// 잘못된 순서
const App = () => {
  const [user, setUser] = useState(null);

  // 이 코드는 마운트 후에 실행
  useEffect(() => {
    const token = localStorage.getItem('token');
    fetchUser(token);
  }, []);

  // 하지만 여기서는 user가 null
  return user ? <Dashboard /> : <Login />;
};

해결책은 로딩 상태를 만드는 것이다.

const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  const token = localStorage.getItem('token');
  if (token) {
    fetchUser(token).finally(() => setLoading(false));
  } else {
    setLoading(false);
  }
}, []);

if (loading) return <div>Loading...</div>;
return user ? <Dashboard /> : <Login />;

배포 환경의 차이

로컬과 배포 환경에서 다를 수 있는 것:

  • 쿠키 도메인 설정
  • HTTPS vs HTTP
  • 다른 포트
  • 다른 API 주소

개발자 도구의 Network 탭에서 요청을 보면, 쿠키가 실제로 전송되는지 확인할 수 있다.

확인 순서

  1. localStorage에 토큰이 저장되는가: 개발자 도구 -> Storage
  2. 페이지 새로고침 후 토큰이 유지되는가
  3. 로드 시 토큰을 읽는 useEffect가 실행되는가: console.log 추가
  4. API 요청이 토큰을 포함하는가: Network 탭 확인
  5. 토큰이 만료되었는가: 토큰 디코딩
  6. 쿠키를 사용한다면, CORS와 credentials 설정 확인

로그인 상태 유지는 프런트엔드와 백엔드가 함께 작동한다. 둘 다 확인해야 한다.