본문 바로가기
Q&A해결2026년 7월 13일1분 읽기

개발모드에서 useEffect가 두 번 실행돼서 API가 두 번 호출되는데 왜 이런가요?

r
react_seojin
조회 1098댓글 2

CRA에서 Vite로 옮기고 React 18로 올린 프로젝트인데요, 컴포넌트 마운트 시 데이터 받아오려고 useEffect 안에서 fetch를 하는데 네트워크 탭을 보면 요청이 딱 두 번씩 나갑니다. 뎁스 배열은 빈 배열 []로 넣었고요.

이상한 건 npm run build로 빌드해서 띄우면 요청이 한 번만 나가요. 개발 서버로 돌릴 때만 두 번입니다. 처음엔 제 로직이 어디서 두 번 호출하나 싶어서 콘솔 찍어보고 별짓 다 했는데 코드상으로는 분명 한 번이거든요.

useEffect(() => {
  fetch(`/api/orders/${id}`)
    .then(r => r.json())
    .then(setData)
}, [])

혹시 StrictMode랑 관련이 있나 싶어서 검색은 해봤는데 정확히 왜 이러는지, 그리고 실무에서 이걸 그냥 둬도 되는 건지 판단이 안 서네요. POST 같은 거였으면 큰일 날 뻔했는데 😅

댓글 2

김김영삼3개월 전
네 짐작하신 대로 StrictMode 때문입니다. React 18 dev에서는 StrictMode로 감싼 트리를 마운트할 때 일부러 마운트 → 언마운트 → 다시 마운트를 한 번 돌려요. 그래서 effect가 setup → cleanup → setup 순으로 두 번 실행됩니다. 프로덕션 빌드에서는 이 이중 호출이 없어서 한 번만 나가는 거고요.

이게 버그가 아니라 의도된 동작입니다. "이 effect가 여러 번 마운트돼도 안전하냐(=cleanup을 제대로 짰냐)"를 개발 단계에서 강제로 검사해주는 거예요. 그래서 대응 방향은 두 번 호출을 막는 게 아니라, cleanup을 제대로 넣어서 두 번 실행돼도 문제가 없게 만드는 겁니다.

fetch라면 AbortController를 쓰는 게 정석이에요. effect 안에서 controller 만들고 signal을 fetch에 넘긴 다음, cleanup에서 controller.abort() 호출하면 첫 요청이 취소되고 마지막 요청 결과만 반영됩니다. 이렇게 해두면 컴포넌트가 빠르게 마운트/언마운트될 때 응답 순서가 꼬여서 옛 데이터가 최신을 덮어쓰는 race condition도 같이 막아줘요.

참고로 StrictMode를 그냥 꺼버리는 분들도 있는데, 개발 편의로 눈속임하는 거라 권하진 않습니다. 실제로 이 이중 호출이 드러내주는 버그(구독 정리 안 함, 타이머 안 지움 같은)가 프로덕션에서 터지는 경우가 많거든요. POST처럼 부수효과가 있는 건 애초에 effect보다 사용자 액션(onClick 등) 핸들러에 두는 게 맞습니다.
rreact_seojin(guest)3개월 전
아 이거였네요. AbortController로 cleanup 붙이니까 dev에서 두 번 나가도 취소돼서 깔끔해졌습니다. StrictMode 끄는 게 능사가 아니란 말씀도 와닿네요. 감사합니다 🙏
Ctrl+Enter로 등록