불필요한 렌더링의 늪

2

발행일: 2025년 05월 06일

시간은 속절없이 흘렀다. ‘아틀라스’ 프로젝트는 초기 단계를 지나 본격적인 기능 구현 페이즈로 접어들었다. 카토와 팀원들은 밤낮없이 키보드를 두드렸고, 기능 목록의 체크 박스는 하나씩 채워져 갔다. 하지만 카토의 미간에 잡힌 주름은 점점 더 깊어질 뿐이었다.

“이상하단 말이지…”

늦은 밤, 사무실에 홀로 남은 카토는 혼잣말을 중얼거리며 마른세수를 했다. 화면 속 애플리케이션은 분명 요구된 기능을 수행하고 있었다. 상품 목록은 잘 보이고, 장바구니에 담기고, 사용자 정보도 문제없이 표시되었다.

하지만 무언가… 매끄럽지 않았다. 마치 잘 닦인 유리창에 미세한 먼지가 내려앉은 듯, 인터랙션이 한 템포씩 굼떴다. 특히 상태 변경이 잦은 페이지에서는 그 미묘한 버벅임이 더욱 도드라졌다.

‘단순히 기능이 많아져서 그런 건 아닐 텐데.’

카토는 브라우저 개발자 도구를 열고 React DevTools 탭을 켰다. 컴포넌트 트리 구조가 복잡하게 얽힌 채 화면을 가득 메웠다. 그는 ‘Highlight updates when components render’ 옵션을 체크했다.

그리고, 사소한 액션을 취해보았다. 예를 들어, 다크 모드/라이트 모드 전환 버튼을 클릭하는 것. 사용자 테마 설정은 최상위 Context Provider에서 관리되고 있었다.

파바박!

순간, 화면의 절반 이상이 번쩍였다. 테마 설정과는 전혀 상관없는 상품 추천 목록, 심지어는 푸터 영역의 컴포넌트까지 리렌더링 알림이 터져 나왔다.

“말도 안 돼…”

카토의 입에서 탄식이 흘러나왔다. 분명 테마 상태만 변경했을 뿐인데, 왜 관련 없는 컴포넌트들까지 다시 그려지는 거지? 그는 컴포넌트 트리를 파고들며 각 컴포넌트가 어떤 Context를 구독하고 있는지 추적하기 시작했다.

문제의 원인은 명확해졌다. Context API의 작동 방식 그 자체였다.

하나의 거대한 Context Provider 안에 여러 상태 값(테마, 사용자 정보, 장바구니 등)을 욱여넣은 것이 화근이었다. Context는 구독하는 컴포넌트에게 ‘내 안의 무언가가 바뀌었다!’고 통째로 알려줄 뿐, ‘정확히 무엇이 바뀌었으니 너만 반응해!’라고 친절하게 알려주지 않았다.

결국, 테마 상태 하나가 바뀌어도 해당 Context를 구독하는 모든 컴포넌트는 일단 깨어났다. 그리고 자신이 사용하는 값이 실제로 변경되었는지 확인하고, 변경되지 않았더라도 일단 리렌더링을 수행하는 경우가 허다했다. 마치 온 동네 스피커로 “집주인 일어났다!”고 외치니, 옆집, 앞집 할 것 없이 모든 주민이 일단 창밖을 내다보는 꼴이었다.

“젠장…!”

카토는 책상을 내리쳤다. 밤샘 디버깅 끝에 마주한 것은 특정 코드의 버그가 아니라, 아키텍처 선택 자체의 근본적인 한계였다.

다음 날 아침, 퀭한 눈으로 출근한 카토는 켄지에게 상황을 설명했다.

“켄지 상, Context 때문에 불필요한 렌더링이 너무 심각합니다. 이대로는 성능 저하를 피할 수 없어요.”

켄지는 잠시 카토의 설명을 듣더니, 예상했다는 듯 차분하게 답했다.

“음… 예상했던 문제입니다, 카토 상. Context API를 사용할 때 흔히 발생하는 일이죠. React.memouseMemo, useCallback 같은 최적화 기법들을 적극적으로 활용해야 합니다. Provider를 분리하는 것도 방법이 될 수 있겠고요.”

켄지의 말은 틀리지 않았다. React가 제공하는 최적화 도구들을 사용하면 분명 개선의 여지는 있었다. 하지만 카토의 마음은 개운치 않았다.

‘최적화를 위한 최적화라… 배보다 배꼽이 더 커지는 기분인데.’

memo로 컴포넌트를 감싸고, useMemo로 값을 메모이제이션하고, useCallback으로 함수를 기억하게 만들고… 그렇게 덕지덕지 최적화 코드를 덧붙이다 보면, 정작 중요한 비즈니스 로직은 뒷전으로 밀려나고 코드는 누더기처럼 변해갈 것이 뻔했다. Provider를 잘게 쪼개는 것도 관리 포인트를 늘릴 뿐, 근본적인 해결책은 아니었다.

그날부터 카토는 ‘최적화’와의 전쟁을 시작했다. 하지만 싸우면 싸울수록 깊은 늪에 빠져드는 기분이었다. 코드는 점점 복잡해지고, 가독성은 떨어졌다. 그럼에도 불구하고 불필요한 렌더링은 완전히 사라지지 않았다. 마치 두더지 잡기 게임처럼, 한 곳을 막으면 다른 곳에서 문제가 터져 나왔다.

“이건 뭔가 잘못됐어…”

키보드 위에서 그의 손가락이 멈췄다. 모니터에 비친 자신의 모습은 지쳐 보였다.

‘간결함과 예측 가능성. 그게 React의 매력 아니었나? 그런데 왜 상태 관리는 이렇게 복잡하고 예측 불가능한 방향으로 흘러가는 거지?’

Context API라는 거대한 그림자는 이제 더 이상 어렴풋한 불안감이 아니었다. 그것은 프로젝트의 발목을 잡고 늘어지는, 명백한 ‘늪’이었다.

카토는 깊은 한숨과 함께 의자에 몸을 기댔다. 이 늪에서 벗어날 방법은 없는 걸까? 아니면, 이 늪 자체가 React 상태 관리의 숙명인 걸까? 그의 머릿속은 다시 한번 복잡한 상념으로 가득 찼다. 출구 없는 미로에 갇힌 기분이었다.