집 나간 네트워크는 돌아왔는데 React.lazy는 왜 안 돌아올까

인터넷 연결이 다시 정상적으로 돌아왔음에도 불구하고 리액트 애플리케이션의 특정 화면이 계속해서 오류를 내뱉는 현상은 많은 개발자를 당황스럽게 만듭니다. 배달의민족 주문 접수 웹뷰를 개발하는 과정에서 네트워크가 끊겼다가 복구된 뒤 다른 화면으로 이동하면 무조건 오류 화면이 나타나는 치명적인 결함을 마주했습니다. 원인을 추적해 보니 이는 단순한 프로그램 오류가 아니라 웹 표준 사양에 명시된 브라우저 고유의 동작 방식 때문이었습니다. 프론트엔드 빌드 도구인 비이트가 권장하는 일반적인 새로고침 방식은 상태가 초기화되는 단일 페이지 애플리케이션 구조상 적용할 수 없었습니다. 이 글에서는 리액트 지연 로딩을 활용하는 과정에서 한 번 실패한 모듈 불러오기를 브라우저가 어떻게 기억하고 차단하는지 그 해결 과정을 상세히 공유합니다. 아무런 에러 로그도 없이 요청조차 보내지 않는 브라우저의 속성을 파헤치며 문제 해결의 실마리를 찾아보겠습니다.

=

집 나간 네트워크는 돌아왔는데 React.lazy는 왜 안 돌아올까

집 나간 네트워크는 돌아왔는데 React.lazy는 왜 안 돌아올까

1. 소제목

1. 소제목
1. 소제목

네트워크는 복구됐는데 화면은 그대로였다

배포일을 불과 이틀 앞둔 긴박한 시점에서 사용자 테스트 과정 중 치명적인 티켓이 접수되었습니다. 태블릿 기기의 네트워크 연결을 의도적으로 차단한 상태에서 다른 화면으로 이동을 시도한 뒤 다시 인터넷을 연결하면 오류 페이지가 고정되는 현상이었습니다. 어느 화면을 눌러도, 몇 번을 다시 시도해도 상황은 전혀 나아지지 않았고 화면 전체가 먹통이 되었습니다. 이미 네트워크 상태는 완벽하게 복구되었음에도 불구하고 브라우저는 과거의 실패 기억에서 벗어나지 못하고 있었습니다. 에러 추적 도구를 살펴보니 코드를 동적으로 불러오지 못했다는 예외 상황이 여러 화면에 걸쳐 무더기로 쌓여 있었습니다. 당시 프로젝트는 화면 단위로 리액트 지연 로딩과 동적 불러오기를 적용하여 초기 구동 속도를 높이는 구조를 취하고 있었습니다.

네트워크가 끊긴 동안 사용자가 아직 내려받지 못한 화면으로 이동하려고 시도하면 당연히 동적 불러오기 작업은 실패로 끝날 수밖에 없습니다. 개발 단계에서 충분히 예측할 수 있었던 정상적인 실패 범위 안의 사건이었습니다. 그러나 진짜 문제는 네트워크가 다시 연결된 이후에도 브라우저가 과거의 실패를 고집스럽게 붙잡고 있다는 점이었습니다. 사용자가 정상적인 환경으로 돌아왔음에도 불구하고 화면은 여전히 에러 상태를 벗어나지 못하는 기이한 현상이 반복되었습니다. 마감 기한이 코앞으로 다가온 상황에서 이 문제를 빠르게 해결하지 못하면 현장의 업주들이 큰 불편을 겪을 것이 명백했습니다. 팀원들과 함께 즉각적인 원인 분석에 돌입하며 브라우저 내부에서 어떤 일이 벌어지고 있는지 추적하기 시작했습니다.

💡 핵심 포인트
네트워크가 복구되어도 화면이 안 되는 문제는 예측 가능한 범위였으나 이후의 비정상적인 반복 실패가 핵심 난제였습니다.

2. 소제목

2. 소제목
2. 소제목

리액트 지연 로딩 재시도가 통하지 않았던 이유

가장 먼저 의심의 눈길을 보낸 곳은 당연히 화면 지연 로딩을 담당하는 리액트 내부의 핵심 기능이었습니다. 공식 문서의 설명에 따르면 관련 약속 객체와 그 결과가 내부적으로 캐시되어 불러오기 함수가 중복으로 호출되지 않는 구조를 가집니다. 만약 작업이 실패할 경우 가장 가까운 오류 경계 컴포넌트로 예외를 전달하도록 설계되어 있습니다. 하지만 공식 문서의 설명 페이지만으로는 한 번 실패한 작업을 다시 수행할 수 있는 명확한 방법을 찾을 수 없었습니다. 답답한 마음에 라이브러리의 원스톱 코드를 직접 열어 내부 작동 방식을 낱낱이 뜯어보기로 결정했습니다.

코드를 살펴보니 지연 로딩 컴포넌트는 내부적으로 특정한 상태 값을 철저하게 관리하고 있었습니다. 초기화되지 않은 상태에서 대기 상태를 거쳐 성공이나 실패 상태로 전환되는 생명주기를 가지고 있었습니다. 한 번 실패 상태로 확정되면 그 이후로는 어떠한 외부 요인으로도 다른 상태로 되돌아갈 수 없는 견고한 방어가 걸려 있었습니다. 그 뒤로는 컴포넌트가 다시 그려질 때마다 캐시된 에러 객체만을 반복해서 던질 뿐 지연 로딩에 넘겨준 함수는 다시 호출되지 않았습니다. 인공지능 도구에 도움을 요청하여 얻은 일반적인 재시도 패턴 코드를 적용해 보았지만 아무런 소용이 없었습니다. 더 이상한 점은 분명히 새로운 불러오기 함수를 호출하고 있음에도 네트워크 요청 기록 창이 완전히 잠잠하다는 사실이었습니다.

💡 핵심 포인트
한 번 실패한 리액트 지연 로딩은 내부 상태가 거부됨으로 고정되어 재시도 함수가 호출되지 않는 구조적 한계가 있었습니다.

3. 소제목

3. 소제목
3. 소제목

브라우저는 왜 네트워크 요청을 아예 보내지 않았을까

새로운 코드가 분명히 실행되고 있음에도 불구하고 네트워크 요청 탭에 어떠한 흔적도 남지 않는 것은 매우 이례적인 현상입니다. 브라우저 자체가 아예 요청을 시도조차 하지 않고 즉각적으로 똑같은 에러를 돌려주고 있다는 명확한 증거였습니다. 네트워크 요청을 건너뛰고 곧바로 거부 반응을 보인다는 것은 브라우저의 어딘가에 이미 실패 사실이 단단히 기록되어 있음을 의미했습니다. 리액트의 상태를 완전히 초기화하고 처음부터 다시 시작해 보아도 새로 호출된 기능은 어김없이 즉시 거부되었습니다. 이쯤 되니 실패를 기억하고 있는 주체는 리액트 내부가 아니라 그보다 더 근본적인 브라우저 계층일 것이라는 확신이 들었습니다.

사용자가 경험하는 웹 화면 뒤편에서 브라우저는 수많은 모듈 파일을 관리하며 최적의 효율을 내기 위해 애를 씁니다. 개발자가 작성한 자바스크립트 모듈들이 원활하게 작동하도록 돕는 보이지 않는 장치들이 사방에 존재합니다. 리액트가 호출하는 방식은 결국 브라우저가 제공하는 근본적인 모듈 불러오기 기능을 기반으로 작동할 수밖에 없습니다. 따라서 브라우저가 네트워크 통신을 아예 생략하고 에러를 뱉는 이유는 브라우저가 관리하는 메모리 공간 어딘가에 해당 주소가 불가능하다고 낙인이 찍혔기 때문입니다. 이 추론을 바탕으로 웹 표준 규격서와 브라우저 엔진의 작동 원리를 깊이 있게 파고들기 시작했습니다.

💡 핵심 포인트
브라우저가 요청조차 보내지 않고 에러를 반환하는 것은 리액트가 아니라 브라우저 자체의 캐시 메커니즘 때문입니다.

4. 소제목

4. 소제목
4. 소제목

모듈 맵은 실패까지 기억하는 강력한 장치

자바스크립트 모듈 생태계에는 문서 단위로 작동하는 강력한 캐시 저장소인 모듈 맵이라는 개념이 존재합니다. 브라우저는 불필요한 중복 다운로드를 막기 위해 모듈을 웹 주소 단위로 이 저장소에 철저하게 기록해 둡니다. 똑같은 주소로 다시 불러오기를 시도하면 네트워크를 타는 대신 모듈 맵에 안전하게 보관된 기존 결과를 즉시 반환합니다. 동일한 모듈 파일이 브라우저 내부에서 두 번 이상 중복으로 평가되는 것을 막아주는 매우 훌륭하고 효율적인 장치입니다.

문제의 원인을 추적하던 당시의 웹 표준 사양서를 살펴보면 모듈 맵이 품고 있는 데이터의 종류에 흥미로운 항목이 있었습니다. 그것은 바로 불러오기 과정에서 실패한 경우를 나타내는 별도의 빈 값 표현이 정식으로 포함되어 있었다는 사실입니다. 즉 모듈 맵이 성공뿐만 아니라 실패의 기록까지 고스란히 캐시하는 것은 브라우저나 특정 도구의 버그가 아니라 표준 사양에 명시된 정상적인 동작이었습니다. 브라우저 제작사들은 이 사양을 한 치의 오차도 없이 충실하게 따르고 있었기에 오프라인 상황에서의 실패를 영구적으로 기억했던 것입니다. 한 번 실패의 낙인이 찍힌 모듈 주소는 해당 문서가 유지되는 동안 실패 상태로 꽁꽁 얼어붙게 됩니다.

💡 핵심 포인트
웹 표준 사양에 따라 모듈 맵이 실패 결과까지 캐시하기 때문에 오프라인 실패 주소는 계속해서 에러를 반환합니다.

5. 소제목

5. 소제목
5. 소제목

표준의 변화와 현업 개발자의 치열한 대안 찾기

다행히도 웹 표준을 관리하는 국제 기구에서는 이러한 현상이 가져오는 실무적 불편함을 뒤늦게 인지하고 사양을 개정했습니다. 과거에는 실패한 동적 불러오기가 영구적으로 모듈 맵을 오염시켰지만 현재는 해당 규정이 삭제되어 개선된 상태입니다. 그러나 사양이 바뀐다고 해서 당장 전 세계 사용자의 구형 브라우저가 한 번에 업데이트되는 것은 아니므로 현업 개발자는 여전히 스스로 해결책을 찾아야 했습니다. 앱을 완전히 새로고침하는 방법은 단일 페이지 애플리케이션의 특성상 사용자 경험을 크게 해치므로 절대 선택할 수 없는 카드였습니다.

결국 브라우저의 모듈 맵이 실패를 기억하는 방식을 우회하거나 무력화할 수 있는 영리한 트릭이 필요했습니다. 동적 불러오기를 수행할 때 주소 뒤에 시시각각 변하는 무작위 값을 매개변수로 덧붙여 브라우저가 완전히 새로운 주소로 인식하게 만드는 방법을 고안했습니다. 이렇게 하면 모듈 맵의 기존 캐시를 자연스럽게 비껴가면서 네트워크를 통해 다시 파일을 안전하게 내려받을 수 있게 됩니다. 수많은 테스트를 거쳐 이 방식을 적용한 결과 오프라인에서 온라인으로 전환된 직후에도 화면이 매끄럽게 복구되는 것을 확인했습니다. 사용자에게 새로고침을 강요하지 않고도 문제를 우아하게 해결할 수 있었던 소중한 경험이었습니다.

💡 핵심 포인트
표준 사양 개정 전까지는 주소 변형 등의 기법을 통해 브라우저 캐시를 우회하는 실전 대응책이 필수적이었습니다.

6. 소제목

6. 소제목
6. 소제목

기술적 한계를 넘어 더 단단한 앱으로 나아가는 길

프론트엔드 개발을 하다 보면 때로는 나의 코드나 라이브러리가 아니라 브라우저의 거대한 표준 사양과 부딪히는 순간을 마주합니다. 이번 경험은 단순한 버그 수정에 그치지 않고 브라우저가 웹 자원을 어떻게 관리하고 기억하는지 그 밑바닥을 들여다보는 소중한 계기가 되었습니다. 프레임워크가 제공하는 편리한 기능 이면에 숨겨진 웹 표준의 동작 방식을 정확히 이해하는 것이 얼마나 중요한지 깨달았습니다. 앞으로 더욱 복잡해지는 웹 애플리케이션 환경에서 예외 상황에 유연하게 대처할 수 있는 탄탄한 시야를 넓혔습니다.

독자 여러분께서도 운영 중인 서비스에서 원인을 알 수 없는 네트워크 관련 오류나 화면 멈춤 현상을 겪는다면 브라우저의 근본적인 저장소를 의심해 보시기 바랍니다. 프레임워크의 한계를 탓하기 전에 웹 표준이 정한 규칙과 브라우저의 메모리 관리 방식을 차근차근 짚어보면 실마리가 보일 것입니다. 완벽해 보이는 현대의 개발 도구들도 저마다의 역사와 제약을 가지고 있음을 기억하면 문제 해결이 한결 수월해집니다. 앞으로도 현장에서 부딪히는 크고 작은 기술적 난관들을 깊이 있는 분석과 창의적인 방법으로 멋지게 헤쳐 나가시기를 응원합니다.

💡 핵심 포인트
브라우저와 웹 표준에 대한 깊은 이해는 복잡한 프론트엔드 오류를 해결하는 가장 강력한 무기입니다.

자주 묻는 질문

네트워크가 복구되었는데도 왜 화면이 계속 오류를 내뱉나요?
브라우저의 모듈 맵이 과거의 실패 기록까지 캐시하기 때문에 네트워크 요청조차 보내지 않고 즉시 에러를 반환하기 때문입니다.
리액트 지연 로딩을 재시도하는 코드가 왜 소용이 없었나요?
리액트 내부 상태가 한 번 실패로 고정되면 자체적으로 에러를 반복해서 던질 뿐 실제 불러오기 함수를 다시 실행하지 않기 때문입니다.
단일 페이지 애플리케이션에서 새로고침을 쓸 수 없는 이유는 무엇인가요?
새로고침을 수행하면 사용자가 입력하던 앱의 현재 상태와 데이터가 모두 초기화되어 심각한 사용자 경험 저하를 유발하기 때문입니다.
모듈 맵의 실패 캐시 문제를 해결하는 실전 방법은 무엇인가요?
동적 불러오기 주소 뒤에 고유한 무작위 값을 덧붙여 브라우저가 완전히 새로운 주소로 인식하게 우회하는 방식을 사용합니다.

=