웹 개발
파일 업로드 후 로그인이 자꾸 풀리는 문제
같은 기능도 로컬과 배포 환경에서 다르게 작동한다. 특히 파일 업로드와 인증 관련 요청이 그렇다.
사용자와 개발자가 보는 것이 다르다
로컬에선 파일을 업로드한 후에도 로그인 상태가 유지되는데, 운영에선 자꾸 로그아웃된다. 화면은 같은데 동작은 다르다.
먼저 모바일과 데스크톱에서 각각 테스트한다.
# 모바일 폭에서 보기 (Chrome DevTools)
# 반응형 설계 모드: Ctrl+Shift+M
Modal이나 overlay의 렌더링이 달라서 문제일 수 있다.
파일 업로드 요청이 실제로 뭔지 본다
Browser DevTools에서 Network 탭을 연다.
POST /api/upload
Content-Type: multipart/form-data
Content-Length: 5242880 # 5MB
파일 크기, 타임아웃, 응답 코드를 확인한다. 특히:
- 308 Resume Incomplete: 업로드가 끝나지 않았다
- 413 Payload Too Large: 서버 제한 초과
- 413 Request Entity Too Large: Nginx 제한 초과
쿠키와 토큰 상태 확인
# 브라우저 DevTools > Application > Cookies
# 또는 로컬 스토리지, 세션 스토리지
파일 업로드 후에 다음을 확인한다.
- 인증 토큰 (Token/JWT): 아직 있는가
- Session Cookie: HttpOnly flag가 있는가
- CSRF Token: 유효한가
특히 HttpOnly 쿠키는 JavaScript로 접근할 수 없다. 그래서 개발자 도구에서도 안 보인다.
# curl로 헤더 확인
curl -i -F "[email protected]" https://example.com/api/upload
# Set-Cookie 헤더 확인
# Set-Cookie: session=...; HttpOnly; Secure; SameSite=Strict
파일 업로드 중 렌더링 상태
Large file upload는 시간이 걸린다. 그 동안 다른 요청을 보낼 수 있다.
// React 예시
const [uploading, setUploading] = useState(false);
const handleUpload = async (file: File) => {
setUploading(true);
// 업로드 중에 다른 요청을 방지해야 한다
if (uploading) return;
try {
await uploadFile(file);
} finally {
setUploading(false);
}
};
업로드 중에 다른 API 요청이 발생하면, 토큰 갱신이 충돌할 수 있다.
로드밸런서와 세션 일관성
운영 환경에서 여러 서버가 있다면, 같은 사용자가 다른 서버에 요청을 보낼 수 있다.
# 요청이 어느 서버에 가는지 확인
curl -i https://example.com/api/upload | grep -E "Server|X-Server"
Session store가 서버 메모리에만 있으면 서버 A에 업로드하고 서버 B에서 확인할 때 세션이 없을 수 있다.
이를 해결하려면:
- 공유 Redis: 모든 서버가 같은 Redis에서 세션을 읽음
- 데이터베이스 세션 저장소: 세션을 DB에 저장
- JWT: 토큰 기반 인증 (상태 없음)
Nginx 설정 확인
# 파일 업로드 크기 제한
sudo grep "client_max_body_size" /etc/nginx/nginx.conf
# 타임아웃 설정
sudo grep -E "proxy_connect_timeout|proxy_read_timeout" /etc/nginx/nginx.conf
Nginx가 업로드를 중단했는데, 애플리케이션은 계속 대기할 수 있다. 그 동안 인증 정보가 만료될 수 있다.
모바일에서 특히 조심할 것
모바일 네트워크는 예측 불가능하다.
# Chrome DevTools > Network > Throttle
# "Slow 3G" 등으로 느린 네트워크를 시뮬레이션
Wi-Fi에선 잘 작동해도 3G/LTE에선 타임아웃이 날 수 있다. Upload 중에 네트워크가 끊겼다가 복구되면 재시도 로직이 필요하다.
const uploadWithRetry = async (file: File, maxRetries = 3) => {
let retries = 0;
while (retries < maxRetries) {
try {
return await uploadFile(file);
} catch (error) {
retries++;
if (retries >= maxRetries) throw error;
// 지수 백오프: 1초, 2초, 4초
await new Promise(r => setTimeout(r, Math.pow(2, retries) * 1000));
}
}
};
최종 점검
문제를 수정한 후 확인한다.
- 로컬 환경에서 파일 업로드 후 세션 유지 확인
- 모바일과 데스크톱에서 각각 확인
- 느린 네트워크 시뮬레이션 후 확인
- 멀티 서버 환경에서 세션 일관성 확인
이 정도 확인이 끝나면 대부분의 사용자가 원활하게 경험할 수 있다.