SSE (Server-Sent Events)를 React 애플리케이션에 적용하면서 발생한 문제들과 해결 방법을 정리
SSEProvider가 App.js의 Router 전체를 감싸고 있어 로그인/회원가입 페이지에서도 SSE가 실행됨.✅ 로그인 및 회원가입 페이지는 SSEProvider에서 제외하고, 인증된 페이지에만 적용
<Router>
<Routes>
{/* ✅ 로그인 & 회원가입에서는 SSEProvider 제외 */}
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
{/* ✅ 나머지 페이지에서는 SSEProvider 적용 */}
<Route
path="/*"
element={
<SSEProvider>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/store/:storeId" element={<StoreDetails />} />
{/* ...생략 */}
</Routes>
</SSEProvider>
}
/>
</Routes>
</Router
SSEProvider의 useEffect가 여러 번 실행되어 기존 SSE 연결을 닫지 않은 채 새로운 연결이 계속 생성됨.✅ eventSourceRef를 useRef로 관리하여 중복 연결 방지
✅ 기존 SSE 연결이 있으면 새 연결을 생성하지 않도록 조건 추가
onst eventSourceRef = useRef(null);
const connectSSE = () => {
if (eventSourceRef.current) {
console.log("⚠️ 기존 SSE 연결 존재, 중복 연결 방지");
return;
}
eventSourceRef.current = new EventSourcePolyfill(
"http://localhost:8080/notifications/subscribe",
{
headers: { Authorization: `Bearer ${localStorage.getItem("accessToken")}` },
withCredentials: true,
}
);
eventSourceRef.current.onopen = () => {
console.log("✅ SSE 연결 성공");
};
eventSourceRef.current.onerror = () => {
console.error("❌ SSE 연결 오류 발생, 재연결 시도...");
eventSourceRef.current?.close();
eventSourceRef.current = null;
setTimeout(connectSSE, 3000); // 3초 후 재연결
};
};
useEffect(() => {
connectSSE();
return () => {
console.log("🛑 SSE 연결 해제");
eventSourceRef.current?.close();
eventSourceRef.current = null;
};
}, []);
✅ 이제 페이지 이동 시에도 기존 SSE가 유지되며, 중복 연결이 방지됨!
✅ SSE 에러 발생 시 자동 재연결 로직 구현
✅ 최대 3회까지 재연결을 시도하고, 실패 시 토큰 갱신 후 재시도
const retryCountRef = useRef(0);
const connectSSE = () => {
if (eventSourceRef.current) {
console.log("⚠️ 기존 SSE 연결 존재, 중복 연결 방지");
return;
}
eventSourceRef.current = new EventSourcePolyfill(
"http://localhost:8080/notifications/subscribe",
{
headers: { Authorization: `Bearer ${localStorage.getItem("accessToken")}` },
withCredentials: true,
}
);
eventSourceRef.current.onopen = () => {
console.log("✅ SSE 연결 성공");
retryCountRef.current = 0; // 재연결 카운트 초기화
};
eventSourceRef.current.onerror = async (error) => {
console.error("❌ SSE 연결 오류 발생:", error);
eventSourceRef.current?.close();
eventSourceRef.current = null;
if (retryCountRef.current >= 3) {
console.warn("🚨 SSE 재연결 3회 실패, 토큰 갱신 후 재시도");
const success = await getRefreshToken();
if (success) connectSSE();
} else {
setTimeout(() => {
retryCountRef.current += 1;
console.log(`🔄 SSE 재연결 (${retryCountRef.current}번째 시도)`);
connectSSE();
}, 3000);
}
};
};
✅ 이제 SSE 연결이 끊겨도 자동으로 재연결됨!
✅ 401 오류 발생 시 자동으로 리프레시 토큰을 요청하고, 성공하면 SSE 재연결
✅ 토큰 갱신 요청의 중복 실행을 방지하도록 isRefreshingRef 활용
const isRefreshingRef = useRef(false);
const getRefreshToken = async () => {
if (isRefreshingRef.current) return false;
isRefreshingRef.current = true;
try {
console.log("🔄 토큰 갱신 시도...");
const response = await instance.post("/users/refresh", {}, {
headers: { "Content-Type": "application/json" },
withCredentials: true,
});
const newAccessToken = response.data.accessToken;
if (!newAccessToken) throw new Error("새로운 토큰 없음");
localStorage.setItem("accessToken", newAccessToken);
console.log("✅ 토큰 갱신 성공");
return true;
} catch (err) {
console.error("❌ 토큰 갱신 실패, 로그인 페이지로 이동");
navigate("/login");
return false;
} finally {
isRefreshingRef.current = false;
}
};
eventSourceRef.current.onerror = async (error) => {
console.error("❌ SSE 연결 오류 발생:", error);
eventSourceRef.current?.close();
eventSourceRef.current = null;
if (error.status === 401) {
const success = await getRefreshToken();
if (success) {
console.log("🔄 SSE 재연결 시도...");
connectSSE();
}
} else {
setTimeout(() => {
console.log("🔄 SSE 자동 재연결...");
connectSSE();
}, 3000);
}
};
✅ 이제 토큰이 만료되어도 자동으로 갱신되고 SSE가 재연결됨!
| 문제 | 해결 방법 |
|---|---|
| 로그인/회원가입에서도 SSE 실행됨 | 로그인/회원가입에서는SSEProvider제외 |
| 중복 SSE 연결 발생 | useRef를 사용해 중복 연결 방지 |
| SSE가 끊겨도 자동 재연결 안 됨 | 최대 3번 재연결 후 토큰 갱신 후 재시도 |
| 액세스 토큰 만료 시 401 오류 반복 | 401 발생 시 토큰 갱신 후 SSE 재연결 |