본문 바로가기
웹개발 이모저모

[실무에서 복붙해 쓰는 에러 해결 및 트러블슈팅 가이드 #02] 내 노트북 이륙 막기! React 무한 렌더링(Infinite Loop) 구출 작전

by 코드메이트 2026. 3. 19.

안녕하세요! 지난번 "Cannot read properties of undefined" 에러 해결편은 많은 분들의 칼퇴에 도움이 되셨나요? 오늘은 프론트엔드 개발자라면 누구나 한 번쯤 겪어봤을 법한 아주 오싹한 경험을 이야기해 보려고 합니다.

열심히 코딩을 하고 저장(Ctrl+S)을 누른 순간, 갑자기 조용하던 노트북 쿨러가 비행기 이륙하는 소리(위이잉-!)를 내며 미친 듯이 돌아가기 시작합니다. 브라우저 탭은 하얗게 굳어버리고, 새로고침을 눌러도 먹통이 되어 결국 크롬 강제 종료를 눌러야 했던 경험. 네, 맞습니다. 바로 공포의 '무한 렌더링(Infinite Loop)' 즉, Too many re-renders 에러에 당첨되신 겁니다.

대체 멀쩡하던 내 코드가 왜 브라우저를 다운시켜 버렸는지, 실무에서 가장 자주 마주치는 무한 렌더링의 원인과 족집게 해결책을 속 시원히 파헤쳐 보겠습니다.

 

크롬 브라우저 콘솔창에 나타난 Too many re-renders. React limits the number of renders 무한 렌더링 에러 메시지 캡처

 

1. 무한 렌더링, 도대체 왜 일어나는 걸까?

리액트(React)가 일하는 핵심 원리는 아주 단순합니다. "상태(State)가 변하면, 화면을 다시 그린다(Re-render)"입니다. 그런데 만약 '화면을 다시 그리는 과정' 안에서 '상태를 또 변경'하게 되면 어떤 일이 벌어질까요?

상태가 변했으니 리액트는 화면을 다시 그립니다. -> 화면을 그리다 보니 상태가 또 변합니다. -> 어? 또 변했네? 다시 그립니다. -> 또 변합니다. -> 무한 반복...

결국 브라우저의 메모리가 버티지 못하고 뻗어버리는 것이죠. 컴퓨터 입장에서는 주인이 시킨 대로 정말 열심히, 0.001초도 쉬지 않고 화면을 다시 그리고 있었을 뿐입니다.

 

2. 단골손님 1: 이벤트 핸들러에 함수를 "실행"해 버렸을 때

초보 시절에 가장 많이, 그리고 어이없게 당하는 무한 루프의 주범은 바로 온클릭(onClick) 같은 이벤트 핸들러입니다. 코드를 한 번 볼까요?

 

// 브라우저를 뻗게 만드는 무서운 코드 (X)
<button onClick={setCount(count + 1)}>클릭해서 숫자 올리기</button>

// 마음이 편안해지는 올바른 코드 (O)
<button onClick={() => setCount(count + 1)}>클릭해서 숫자 올리기</button>

 

차이가 보이시나요? 위쪽 코드처럼 작성하면, 사용자가 버튼을 '클릭했을 때' 상태를 변경하는 것이 아니라, 리액트가 컴포넌트를 '렌더링하면서' 저 함수를 바로 실행해 버립니다. 렌더링 중에 setCount가 실행되니 State가 변하고, State가 변하니 다시 렌더링하고, 또 setCount가 실행되는 지옥의 굴레에 빠지는 것이죠. 이벤트 핸들러에는 반드시 '함수 그 자체(콜백 함수)'를 넘겨주어야 한다는 점, 잊지 마세요!

 

3. 단골손님 2: 빠져나올 수 없는 useEffect 의존성 배열 지옥

실무에서 주니어, 시니어 할 것 없이 가장 머리를 쥐어뜯게 만드는 범인은 단연 useEffect입니다. API를 호출하거나 특정 상태가 변할 때만 코드를 실행하고 싶어서 썼는데, 의도치 않게 무한 루프에 빠지곤 하죠.

 

State 변경과 컴포넌트 리렌더링, 그리고 useEffect가 꼬리를 물고 반복해서 실행되는 무한 렌더링 사이클 다이어그램

 

주로 두 가지 경우에 발생합니다. 첫째, 의존성 배열(Dependency Array, [])을 아예 빼먹었을 때. 둘째, 의존성 배열 안에 data라는 State를 넣어두고, useEffect 내부에서 다시 setData로 그 상태를 업데이트할 때입니다.

 

// 무한 루프를 유발하는 useEffect (X)
useEffect(() => {
    // data가 바뀔 때마다 실행되는데, 여기서 다시 data를 바꾸고 있음!
    setData(data + 1); 
}, [data]);

 

4. 실전 해결책: 어떻게 고치고 방어해야 할까?

이러한 useEffect 지옥에서 빠져나오려면 의존성 배열에 들어가는 변수들을 최소화해야 합니다. 또한, 객체(Object)나 배열(Array) 같은 참조 타입 데이터는 내용이 같아도 렌더링될 때마다 새로운 메모리 주소를 가지기 때문에 리액트가 "어? 데이터가 변했네?"라고 착각하여 무한 렌더링을 유발하기 쉽습니다. 이럴 때는 useMemo나 useCallback을 사용해 값을 메모이제이션(기억)해 두는 것이 훌륭한 방어책이 됩니다.

추가로, 실무에서는 ESLint의 eslint-plugin-react-hooks를 반드시 설치하고 exhaustive-deps 규칙을 켜두는 것을 강력히 추천합니다. 코드 편집기에서 무한 루프의 위험이 감지되면 노란 줄로 미리 경고를 해주니까, 노트북이 이륙하기 전에 멱살을 잡고 말릴 수 있거든요.

 

 

마무리하며

오늘은 브라우저를 꽁꽁 얼려버리는 공포의 '무한 렌더링(Infinite Loop)'의 원인과 해결법에 대해 알아보았습니다. 리액트는 거짓말을 하지 않습니다. 그저 우리가 무의식중에 "쉬지 말고 계속 그려!"라고 잘못된 명령을 내렸을 뿐이죠. (웃음) 오늘 살펴본 올바른 이벤트 핸들러 작성법(() =>)과 useEffect의 의존성 배열 관리법을 꼭 숙지하셔서, 다시는 노트북 쿨러가 비명을 지르는 일이 없기를 바랍니다!

 

[다음 포스팅 예고] 무한 렌더링 지옥에서 무사히 탈출하셨나요? 다음 글에서는 프론트엔드 개발자들의 또 다른 골칫거리, "로컬 환경(크롬)에서는 완벽했는데, 아이폰으로 들어가니까 화면이 이상해요!"의 주범을 잡아보겠습니다. 바로 "모바일 웹 사파리(Safari)에서만 발생하는 100vh 스크롤 버그 원인과 완벽 해결법" 편으로 돌아오겠습니다. 모바일 웹뷰 개발을 앞두고 계시거나 UI가 깨져서 고통받고 계신다면 다음 글도 절대 놓치지 마세요! 오늘도 버그 없는 평온한 하루 보내시길 응원합니다!

 

 

[실무에서 복붙해 쓰는 에러 해결 및 트러블슈팅 가이드 #03] 모바일 사파리 100vh 스크롤 버그 해

안녕하세요! 지난번 무한 렌더링 지옥 탈출기는 많은 도움이 되셨나요? 오늘 다룰 주제는 아마 프론트엔드 개발자라면 누구나 한 번쯤 퇴근길 발목을 잡혔을 법한, 아주 악명 높은 녀석입니다.

code-bricks.tistory.com