React·CSS·JS가 거짓말하는 순간들

React·CSS·JS가 거짓말하는 순간들

브라우저가 'Yes'라고 답해도, 런타임이 우아하게 동작해도—그 확신이 무너지는 세 가지 지점을 짚는다.

React 렌더링 최적화 state colocation useMemo @supports 한계 꼬리 재귀 TCO CSS 피처 감지 브라우저 호환성
광고

우리는 매일 '이건 작동한다'는 믿음 위에 코드를 쌓는다. React의 메모이제이션이 렌더링을 막아줄 거라는 믿음, @supports가 브라우저 지원 여부를 정확히 알려줄 거라는 믿음, 꼬리 재귀로 작성하면 스택이 안전할 거라는 믿음. 그런데 그 믿음이 조용히 거짓말을 하고 있다면?

useMemo가 아니라 위치가 문제였다

dev.to에 올라온 React 19.2 기반의 기초 재점검 아티클은 꽤 도발적인 주장을 담고 있다. 많은 개발자들이 성능 문제가 생기면 useMemo, useCallback, React.memo를 꺼내 드는데, 실제로 90%의 렌더링 문제는 state가 어디에 사는지에서 비롯된다는 것이다.

전형적인 사례가 있다. Dashboard 컴포넌트 안에 카운터 state가 있고, 그 아래 ExpensiveChart, ComplexTable, HeavySidebar가 자식으로 달려 있다. 버튼을 누를 때마다 이 모든 자식이 리렌더링된다. count와 아무 관계도 없는데도. 해결책은 useMemo가 아니다. CounterSection이라는 작은 컴포넌트를 쪼개서 state를 그 안으로 밀어넣는 것, 즉 state colocation이다. 이후엔 버튼 클릭이 CounterSection만 건드린다.

이게 왜 중요한가? React Compiler 시대에도 이 원칙은 유효하다. 컴파일러가 자동 메모이제이션을 해준다고 해도, state가 트리 상단에 불필요하게 올라가 있으면 컴파일러가 최적화할 수 있는 범위 자체가 줄어든다. 도구가 좋아질수록 기본기의 영향이 증폭된다. useEffect 의존성 배열 관리나 불필요한 객체 재생성 문제도 마찬가지다. React 19.2의 useEffectEvent가 반응형 로직과 비반응형 로직을 분리해주는 것도 결국 이 방향의 연장선이다.

@supports가 "괜찮아"라고 말하지만, 사실은 아니다

CSS 쪽 이야기는 더 교묘하다. dev.to의 @supports 분석 아티클은 CSS 피처 감지의 근본적 한계를 파헤친다. 핵심은 이거다: @supports는 선언(declaration)이 문법적으로 유효한지를 확인하는 것이지, 특정 셀렉터나 컨텍스트에서 실제로 동작하는지를 확인하지 않는다.

구체적 예시가 인상적이다. li::marker 안에서 content: " - "를 지원하는지 @supports로 물어보면, Chrome·Safari·Firefox 모두 "Yes"라고 답한다. 그런데 Safari는 ::marker 안에서 content 속성을 실제로 지원하지 않는다. 결과적으로 Chrome과 Firefox는 빨간 " - "를 그리고, Safari는 빨간 점(기본 마커)을 그린다. @supports가 통과됐는데도.

중첩된 @supports가 상위 셀렉터의 컨텍스트를 상속하지 않는다는 스펙 동작 방식까지 더하면, 문제는 더 복잡해진다. 저자는 @supports rule(::marker { content: " - " }) 같은 형태로 특정 컨텍스트에서의 실제 동작을 검사할 수 있는 확장 문법이 필요하다고 제안한다. :has()가 '연산 비용이 너무 크다'는 우려에도 불구하고 결국 구현된 것처럼, 이 방향도 언젠가 열릴 수 있다.

TCO는 스펙에 있지만, 엔진에는 없다

JavaScript 재귀 이야기는 이 흐름의 완결편이다. ECMAScript 2015는 strict mode에서 proper tail calls(TCO)를 공식 스펙으로 포함시켰다. 꼬리 재귀로 작성하면 스택 프레임을 재사용해 스택 오버플로우를 막을 수 있다는 이야기다. 그런데 2026년 현재, V8(Chrome·Node·Deno)은 TCO를 구현하지 않고, SpiderMonkey(Firefox)도 마찬가지다. Safari의 JavaScriptCore는 구현했다가 철회한 전력이 있다.

즉, 꼬리 재귀로 구조적으로 완벽하게 작성한 코드도 깊이가 깊어지면 RangeError를 던진다. 스펙이 보장하는 것과 런타임이 실제로 하는 것 사이의 간극이다. 실용적 대안은 명확하다: 반복문(iteration)으로 재작성하거나, 재귀 구조를 유지하고 싶다면 트램펄린 패턴을 쓰는 것이다. 트램펄린은 함수가 결과 대신 다음에 호출할 함수를 반환하게 만들고, 루프가 이를 순차적으로 실행한다. 스택 대신 힙을 쓰는 방식으로 오버플로우를 피한다.

공통된 패턴: 추상화가 현실을 가린다

세 이야기는 하나의 맥락으로 수렴한다. React의 메모이제이션 API, CSS의 피처 쿼리, JS의 TCO 스펙—모두 '이걸 쓰면 된다'는 인터페이스가 실제 동작과 어긋나는 지점이 있다. 그 간극은 도구가 나쁘다는 뜻이 아니다. 추상화가 의도적으로 복잡성을 감추기 때문에 생기는 필연적 비용이다.

AI 워크플로우가 코드 생성을 빠르게 만들수록, 이 간극은 더 빨리 코드베이스 안으로 들어온다. Cursor나 Claude가 useMemo를 손쉽게 추천하거나, @supports를 피처 감지 솔루션으로 생성하거나, 꼬리 재귀 패턴을 제안할 때—그 코드가 왜 동작하고 어디서 무너지는지를 이해하는 사람이 없으면, AI는 자신감 있게 거짓말을 전달하는 중개자가 된다.

실무 체크리스트

세 기사가 주는 실천 포인트를 압축하면 이렇다:

  • React: 메모이제이션보다 state 위치를 먼저 점검하라. useMemo는 2차 방어선이다.
  • CSS: @supports는 문법 유효성 검사기다. 특정 셀렉터 컨텍스트에서의 동작은 보장하지 않는다. 크로스 브라우저 실제 렌더링으로 검증하라.
  • JS 재귀: TCO는 스펙이지 런타임 보장이 아니다. 깊이가 예측 불가하면 반복문 또는 트램펄린을 써라.

결국 기본기가 방어선이다

도구와 스펙이 발전하는 속도는 빠르다. React 19.2의 <Activity />, cacheSignal, Partial Pre-rendering은 모두 강력하다. CSS @scope나 container queries도 상황을 개선하고 있다. 하지만 이 도구들은 기반 동작 방식을 이해한 위에서만 제 역할을 한다. 브라우저와 런타임이 '된다'고 말할 때, 그 말이 정확히 무엇을 의미하는지—그리고 무엇을 의미하지 않는지—를 구분하는 능력이 결국 실무 내성을 만든다.

출처

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