믿었던 UI가 왜 깨지나: 환경별 렌더링·상태 불일치 실전 대응법

믿었던 UI가 왜 깨지나: 환경별 렌더링·상태 불일치 실전 대응법

React Query 캐시 불일치, Svelte 5 반응성 모델, Gmail CSS 박탈까지—세 전선에서 동시에 터지는 프론트엔드 렌더링 지옥 탈출 가이드

React Query 상태 불일치 Svelte 5 runes Gmail CSS 이메일 렌더링 캐시 무효화 프론트엔드 트러블슈팅
광고

"분명히 됐는데, 왜 저쪽 화면엔 안 바뀌어 있어요?"

프론트엔드 개발자라면 이 말 들어봤을 겁니다. 아니, 직접 겪어봤을 겁니다. QA 단계까지 멀쩡했던 화면이 특정 기기, 특정 클라이언트, 특정 네비게이션 흐름에서 갑자기 엉뚱한 데이터를 보여주거나 CSS가 통째로 사라지는 경험. 이게 단순 버그가 아니라는 게 더 짜증스럽습니다. 환경 자체가 다르게 동작하는 거니까요.

오늘은 세 개의 실전 케이스를 묶어서 이야기하려 합니다. React Native의 멀티스크린 상태 불일치, Svelte 5의 반응성 모델 전환, 그리고 Gmail이 이메일 CSS를 조용히 삭제하는 문제. 표면적으로는 전혀 다른 이야기 같지만, 뿌리는 같습니다. "내가 작성한 코드가 실행 환경에서 내 의도대로 동작하리라는 가정"이 얼마나 위험한지에 대한 이야기입니다.

React Query: 격리된 로컬 상태가 만드는 데이터 유령

dev.to에 올라온 실전 사례를 보면, React Native 앱에서 Screen A, B, C가 같은 아이템 목록을 각각 useState로 독립 관리하다 생긴 문제가 적나라하게 나옵니다. Screen A에서 아이템을 완료 처리해도, Screen B는 여전히 미완료 상태. 사용자 입장에선 앱이 고장난 것처럼 보입니다. 실제로도 고장이긴 하죠.

근본 원인은 단순합니다. 각 화면이 서버 상태를 로컬 useState에 복사해서 들고 있으면, 한쪽에서 변경이 생겨도 다른 쪽은 알 방법이 없습니다. "이건 버그가 아니라, 비조율된 로컬 상태가 서버 공유 데이터를 관리할 때 나오는 자연스러운 결과"라는 표현이 정확합니다. 구조적 문제인 거예요.

해결책은 TanStack React Query의 세 가지 패턴으로 정리됩니다.

첫째, 쿼리 키 팩토리. 문자열 리터럴을 코드베이스 여기저기 뿌리는 대신, queryKeys.items.list(userId) 같은 중앙화된 키 체계를 만듭니다. as const 타입 단언으로 TypeScript 추론까지 챙기고, 액션 단위로 무효화 그룹을 정의해두면 나중에 "어, 이거 무효화해야 하는 키가 뭐였지?" 하는 사태를 막을 수 있습니다.

둘째, 자동 무효화 뮤테이션. useMutationonSettled에서 관련 쿼리를 일괄 무효화합니다. 여기서 중요한 포인트가 있는데요—onSuccess가 아니라 onSettled를 써야 합니다. 뮤테이션이 실패했을 때도 무효화가 트리거돼야, 옵티미스틱 업데이트로 미리 바뀐 캐시가 틀린 상태로 굳어버리는 걸 막을 수 있거든요. 이게 React Query v5 권장 패턴이기도 합니다.

셋째, React Native AppState 브리지. 웹 브라우저에서는 refetchOnWindowFocus가 자동으로 작동하지만, React Native에서는 OS 레벨의 앱 상태 변화를 수동으로 focusManager에 연결해줘야 합니다. 이 브리지 없이 쓰면 앱을 백그라운드 갔다 돌아와도 데이터가 갱신 안 되는 상황이 생깁니다. Figma에서 볼 때는 괜찮았는데 실제 디바이스에서 다른 바로 그 케이스죠.

Svelte 5 Runes: 컴파일러가 의존성 추적을 대신 해준다면

Svelte 5의 runes 시스템은 프론트엔드 상태관리 논쟁에 흥미로운 관점을 하나 더 던집니다. HostingSift 팀이 70개 이상의 컴포넌트를 runes 기반으로 구축한 경험을 공유했는데, 핵심은 "컴파일러가 의존성 그래프를 빌드 타임에 만든다"는 점입니다.

React의 useEffect에서 의존성 배열을 수동으로 관리해본 사람이라면 공감할 겁니다. 하나 빼먹으면 stale closure, 하나 더 넣으면 무한 루프. 이걸 ESLint로 잡아도 완벽하지 않고, 팀원마다 이해도가 달라서 리뷰할 때마다 갑론을박이 생기죠. Svelte 5는 이 문제를 컴파일러 레벨에서 처리합니다. $derived 블록 안에서 어떤 $state 변수를 읽는지 컴파일러가 추적하고, 그 변수가 바뀔 때만 재계산합니다. useMemo 없이요.

특히 흥미로운 건 nanostores와의 브리지 패턴입니다. 프레임워크에 무관한 상태 관리 라이브러리를 Svelte 5 컴포넌트와 연결할 때, $effect 안에서 구독하고 클린업 함수를 반환하는 세 줄짜리 패턴이 모든 필터 컴포넌트에 동일하게 적용됩니다. React였으면 useEffect에 의존성 배열 달고, useRef로 레퍼런스 잡고, useCallback으로 감싸고... 여러 개의 useEffect가 난무했을 로직이 여기서는 평탄하게 읽힙니다.

번들 사이즈도 무시 못 할 포인트입니다. 70개 컴포넌트에 완전한 반응성을 갖추고도 25KB. 이 숫자가 말해주는 건, 런타임에서 의존성 추적 오버헤드를 지불하는 대신 컴파일 타임에 최적화된 코드를 생성한다는 겁니다. Lighthouse Core Web Vitals 점수 관리하는 입장에서 번들 사이즈는 항상 예민한 숫자인데, 반응성 모델 선택이 여기까지 영향을 준다는 걸 체감하게 해주는 사례입니다.

Gmail CSS: 당신의 스타일시트는 목적지에 도착 못 했습니다

세 번째 케이스는 조금 결이 다릅니다. 앱 개발이 아니라 이메일 개발 이야기인데, 렌더링 환경 불일치라는 주제에서는 가장 극단적인 사례입니다.

Gmail은 수신된 이메일을 자체 HTML 새니타이저로 처리하면서 다음을 조용히 삭제합니다: @font-face, @media 쿼리(데스크톱), position/z-index 계열, display: grid, box-shadow, transform, transition, 외부 <link> 스타일시트, @import. flexbox는 display: flex만 살아남고 align-items, justify-content, flex-direction은 전부 날아갑니다. flex 컨테이너는 존재하는데 자식 배치 제어는 불가능한 상태.

더 황당한 건 8,192자 제한입니다. <style> 블록이 8,192자를 초과하면 넘치는 부분이 아니라 블록 전체가 삭제됩니다. 디자인 시스템이나 CSS 프레임워크 기반으로 이메일 스타일을 생성하면 손쉽게 넘어가는 한계치예요. DevTools도 없고, 에러 메시지도 없고, 누군가가 스크린샷 보내줄 때까지 알 수가 없습니다.

Outlook 데스크톱은 이보다 더합니다. HTML 렌더링 엔진이 문자 그대로 Microsoft Word입니다. border-radius 없음(버튼이 직사각형으로), display: flex 없음, background-size 불가, max-width/min-width 신뢰 불가. Word가 HTML을 렌더링하는 구조 자체가 HTML 4와 CSS 2 수준에서 멈춰있기 때문입니다. 새 Chromium 기반 Outlook이 롤아웃 중이긴 하지만, 두 버전을 동시에 지원해야 하는 과도기가 당분간은 계속됩니다.

다크 모드는 여기서 한 레이어 더 복잡해집니다. Gmail 웹 데스크톱은 이메일 콘텐츠를 아예 건드리지 않고, Gmail iOS는 전체 색상을 반전시키고, Outlook 데스크톱은 선택적으로 색상을 교체합니다. 세 환경에서 동시에 제대로 보이는 이메일을 만들려면 각각 따로 테스트하는 수밖에 없습니다.

공통 교훈: 환경을 신뢰하지 말고, 검증을 설계에 포함시켜라

세 케이스를 관통하는 공통 메시지가 있습니다. 실행 환경이 당신의 코드를 당신이 의도한 대로 실행해줄 거라는 가정 자체가 리스크라는 것.

React Native는 브라우저의 포커스 이벤트를 모릅니다. 직접 연결해줘야 합니다. Gmail은 당신의 CSS를 자기 기준으로 필터링합니다. Svelte 4의 암묵적 반응성은 복잡해지면 예측이 어렵습니다. 이 중 어느 것도 '버그'가 아닙니다. 모두 각 환경의 '의도된 동작'입니다.

시사점은 명확합니다. 상태 관리는 서버 상태와 클라이언트 상태를 분리해서 각각 적합한 도구(React Query + Zustand)를 쓰고, 캐시 무효화 전략을 뮤테이션 설계 단계부터 포함시켜야 합니다. 반응성 모델은 의존성 추적 방식과 번들 비용을 함께 고려해야 합니다. 이메일 CSS는 caniemail.com을 레퍼런스로 삼고, 테스트 도구 없이 배포하지 않는 원칙을 세워야 합니다.

전망: 환경 불일치는 줄어들지 않는다

솔직히 말하면 이 문제들이 근본적으로 해소될 거라는 기대는 안 합니다. 오히려 실행 환경의 종류는 계속 늘어나고 있어요. 웹, 네이티브, 이메일 클라이언트, TV 앱, 차량용 HMI... 각각의 렌더링 엔진이 CSS와 JavaScript를 다르게 해석합니다.

대신 도구들이 이 간극을 메우는 방향으로 발전하고 있습니다. React Query의 캐시 무효화 패턴은 서버 상태 동기화 문제를 선언적으로 다루게 해주고, Svelte 5의 컴파일러 기반 반응성은 의존성 추적 실수를 원천 차단합니다. 이메일 쪽은 Chromium 기반 Outlook의 확산이 그나마 희망입니다.

결국 프론트엔드 개발자가 해야 할 일은 "내 코드가 맞다"는 확신이 아니라, "이 환경에서 내 코드가 어떻게 동작하는지"를 검증하는 습관을 시스템화하는 것입니다. 1px 어긋난 걸 밤새워 고치는 성격이라면, 환경 가정이 틀렸는지도 같은 열정으로 의심해볼 만합니다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요