Phoenix 프론트엔드 트러블슈팅 정리

윤슬·2025년 11월 4일

⚾ Phoenix 프론트엔드 트러블슈팅 정리 (React 기반)

✨ 한 줄 요약:
React의 렌더링 타이밍과 브라우저 쿠키 반영 시점은 완전히 같지 않다.
AuthProvider, Router, Axios, 그리고 TTS/STT의 동작 순서를 의식적으로 설계해야
세션 기반 인증이 안정적으로 유지된다.


1. 전역 상태 관리 (AuthProvider / Session 유지)

문제 상황

  • 로그인 후 새로고침 시 로그인 상태가 사라짐
  • useAuth() 훅에서 상태를 유지하지 못하고 null로 초기화됨
  • 쿠키는 브라우저에 남아있지만, React의 전역 상태가 리셋되어버림

원인 추론

  • React 전역 상태(useState)는 새로고침 시 메모리에서 사라짐
  • Session 기반 인증 구조인데도, 새로고침 후 백엔드 세션 검증 로직이 호출되지 않음
  • Axios 기본 인스턴스에 withCredentials 누락

해결 방안

  • /members/info fetch로 상태 복원

1) 전역 AuthProvider 개선

// src/api/loginstate.jsx
useEffect(() => {
  const fetchUser = async () => {
    try {
      const res = await api.get("/members/info", { withCredentials: true });
      if (res.data.success) setUser(res.data.data);
    } catch {
      setUser(null);
    }
  };
  fetchUser();
}, []);

2) Axios 인스턴스에 쿠키 옵션 상시 적용

// src/api/axiosInstance.js
const api = axios.create({
  baseURL: "http://localhost:8080",
  withCredentials: true,
});

2. React Router + 전역 상태 동기화 문제

문제 상황

로그인 후 /mypage로 이동했는데, user가 null로 남아있음

로그인 성공 직후 navigate("/mypage") 실행 시, 아직 user 상태가 세팅되지 않아
페이지가 다시 리다이렉트됨 (ProtectedRoute에서 user === null로 판단)

원인 추론

AuthProvider.login()은 비동기적으로 상태를 갱신하므로
navigate()가 먼저 실행되면 아직 user가 업데이트되지 않은 상태임

해결 방안

// LoginForm.jsx
const handleLogin = async (e) => {
  e.preventDefault();
  try {
    const response = await api.post("/members/login", { mid, password_hash: password }, { withCredentials: true });
    const member = response.data.data;
    if (member) {
      login(member); // 상태 먼저 반영
      setTimeout(() => navigate("/mypage"), 100); // navigate 약간 지연
    }
  } catch {
    alert("로그인 실패");
  }
};

상태 업데이트(setState)는 즉시 반영되지 않는다.
로그인 직후 라우팅 시에는 약간의 딜레이 또는 useEffect 기반 감지를 사용하자.

3. 보호 라우트(ProtectedRoute) 렌더링 타이밍 문제

문제 상황

AuthProvider가 user를 아직 가져오기 전인데
ProtectedRoute에서 user === null로 판정되어 /login으로 강제 이동

해결 방안

// ProtectedRoute.jsx
export default function ProtectedRoute({ element }) {
  const { user } = useAuth();
  const location = useLocation();

  if (user === undefined) return null; // 아직 fetchUser 중일 때 로딩 차단
  if (user === null) return <Navigate to="/login" state={{ from: location }} replace />;
  return element;
}

undefined 상태를 별도로 두면 초기 fecth 시점의 '로딩' 상태와
진짜 로그아웃 상태(null)를 명확히 구분할 수 있다.

4. TTS (음성 안내 기능) 충돌 문제

문제 상황

페이지 이동 시 이전 음성이 끊기지 않고 겹쳐서 출력

버튼 클릭 시마다 speechSynthesis.speak()가 중첩 실행됨

원인 추론

SpeechSynthesisUtterance 인스턴스가 여러 번 생성됨

언마운트 시점에 speechSynthesis.cancel()이 호출되지 않아 누적됨

해결 방안

  • 실행 전 cancel 호출
// TTS 유틸 함수
const speak = (text) => {
  window.speechSynthesis.cancel(); // 이전 음성 중단
  const utter = new SpeechSynthesisUtterance(text);
  utter.lang = "ko-KR";
  utter.rate = 0.9;
  utter.pitch = 1.0;
  utter.volume = 1.0;
  window.speechSynthesis.speak(utter);
};

5. STT (SpeechRecognition) 중복 인식 및 끊김 문제

문제 상황

자동 예매 화면(SeniorSeatAuto.jsx)에서
SpeechRecognition이 중복 실행되어 “매수를 선택해주세요”가 반복 출력됨.

Chrome에서 not-allowed 에러 발생 (SpeechRecognitionErrorEvent)

원인 추론

컴포넌트 리렌더링 시 recognition.start()가 중복 호출됨

브라우저 마이크 권한이 즉시 허용되지 않아 초기화 실패

해결 방안

  • ref 플래그로 제한
const sttRestarting = useRef(false);

useEffect(() => {
  if (!recognition) {
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    const recog = new SpeechRecognition();
    recog.lang = "ko-KR";
    recog.onresult = handleResult;
    recog.onerror = (e) => console.error("음성 인식 오류:", e);
    setRecognition(recog);
  }
}, []);

const startListening = () => {
  if (!sttRestarting.current) {
    sttRestarting.current = true;
    recognition.start();
    setTimeout(() => (sttRestarting.current = false), 1500); // 재시작 간격 제한
  }
};

참고 : window.SpeechRecognition은 Chrome 보안 정책상 HTTPS 환경 또는 localhost에서만 동작한다.

6. useEffect 반복 호출 문제

문제 상황

useEffect 내부의 TTS 또는 Axios 호출이 무한 루프로 실행됨

“한 번만 실행되어야 하는” 코드가 여러 번 호출되어 브라우저 과부하

원인 추론

useEffect의 의존성 배열 누락 또는 잘못된 상태 포함

상태 업데이트(setState)가 트리거되어 다시 useEffect 호출됨

해결 방안

  • 빈 배열 유지
// 잘못된 예시 
useEffect(() => {
  speak("안내 음성입니다.");
  setCount(count + 1); // 상태 변경 → 재렌더 → 무한 루프
}, [count]);

// 수정된 예시 
useEffect(() => {
  speak("안내 음성입니다.");
  // 빈 배열 → 최초 렌더링 시 1회만 실행
}, []);
profile
우상향그래프

0개의 댓글