React와의 통합: 스냅샷의 필요성
제7화
발행일: 2025년 05월 29일
Valtio의 심장은 뛰고 있었다. proxy()
함수로 감싸인 상태 객체는 마치 살아있는 유기체처럼, 가해지는 모든 변경을 감지하고 내부적으로 알림을 발생시켰다. 다이시 카토는 자신의 프로토타입을 보며 만족감에 젖어 있었다. 개발자는 그저 state.count++
나 state.user.name = '...'
같은 지극히 평범한 코드를 작성하면 되었다. 번거로운 set
함수나 Immer의 도움 없이도 상태 변경이 가능해진 것이다.
"이 직관성… 이 간결함…! 드디어 해냈어."
이제 남은 것은 이 강력한 엔진을 React라는 거대한 마차에 연결하는 일이었다. 그는 자신만만하게 간단한 React 컴포넌트를 작성하기 시작했다. Valtio 상태를 직접 가져와 화면에 표시하고, 버튼 클릭 시 상태를 직접 변경하는 코드였다.
import React from 'react';
import { state } from './store'; // Valtio 프록시 상태 가져오기
function Counter() {
// !!! 문제의 시작 !!!
// 프록시 상태를 직접 컴포넌트에서 사용하려고 한다!
const currentCount = state.count;
const handleClick = () => {
// 상태를 직접 변경! Valtio의 장점!
state.count++;
console.log('카운터 증가! 현재 값:', state.count); // 콘솔에는 잘 찍힌다!
};
return (
<div>
<p>Count: {currentCount}</p> {/* 화면에는 반영이...? */}
<button onClick={handleClick}>증가</button>
</div>
);
}
코드는 간결하고 아름다웠다. Valtio가 추구하는 직관성이 그대로 드러나는 듯했다. 그는 기대감에 부풀어 코드를 실행했다. 버튼을 클릭하자 콘솔에는 "카운터 증가!" 메시지와 함께 변경된 값이 정확히 찍혔다. Proxy는 제 역할을 완벽히 수행하고 있었다.
하지만… 화면은 미동조차 하지 않았다.
Count: 0
버튼을 아무리 눌러도 화면의 숫자는 요지부동이었다. 콘솔에서는 분명 state.count
값이 계속 증가하고 있는데, 정작 React 컴포넌트는 그 변화를 전혀 감지하지 못하고 있었다.
"어째서…?"
카토의 얼굴에서 미소가 사라졌다. 식은땀이 등줄기를 타고 흘렀다. 그는 잠시 숨을 고르며 문제의 근원을 파고들었다.
React의 작동 방식. 그것이 문제였다. React는 효율적인 렌더링을 위해 상태나 props의 참조(reference)가 변경되었는지를 확인한다. 이전 상태 객체와 새로운 상태 객체가 메모리상에서 서로 다른 주소를 가리킬 때, React는 '아, 상태가 바뀌었구나!'라고 인지하고 해당 컴포넌트를 다시 그린다. 이것이 바로 불변성(Immutability)이 중요한 이유였다. set(state => ({ ...state, count: state.count + 1 }))
같은 코드는 매번 새로운 객체를 생성하여 반환함으로써 React에게 명확한 변경 신호를 보냈던 것이다.
하지만 Valtio의 프록시 상태는 달랐다. state.count++
는 기존 state
객체 내부의 값만 바꿀 뿐, state
객체 자체의 참조는 그대로 유지한다. React 입장에서는 컴포넌트가 여전히 동일한 객체를 바라보고 있으니, 아무런 변화도 감지하지 못하는 것이 당연했다.
"크윽…! 이걸 간과했군."
카토는 머리를 감쌌다. Valtio의 가장 큰 장점인 '직접 변경 가능(Mutable)'이라는 특성이, React와의 통합에서는 치명적인 단점이 되어 돌아온 것이다. 마치 물과 기름처럼, 가변적인 Valtio 상태와 불변성을 선호하는 React는 근본적으로 섞이기 어려운 존재처럼 보였다.
절망감이 그를 엄습했다. 여기서 포기해야 하는가? '직관적인 변경'이라는 꿈을 접고, 결국에는 Zustand나 Jotai처럼 불변성을 강제하는 방식으로 돌아가야 하는가?
아니, 그럴 수는 없었다. 그는 이미 Proxy가 선사하는 개발 경험의 혁신을 목격했다. 이대로 물러설 수는 없었다. 반드시 방법이 있을 터였다.
"React가 원하는 것은 결국 '변경되었다'는 신호… 그리고 그 신호는 '새로운 참조'를 통해 전달된다."
그렇다면, Valtio가 React에게 '새로운 참조'를 제공해주면 되지 않을까? 하지만 어떻게? 원본 프록시 상태는 계속 가변적으로 유지하면서 말이다.
그의 머릿속에서 번개가 쳤다.
"스냅샷(Snapshot)…!"
그래, 스냅샷이다! 마치 사진을 찍듯이, 특정 시점의 프록시 상태를 그대로 복사한 불변의 복제본을 만드는 것이다. 컴포넌트는 이 '스냅샷'을 사용한다.
Valtio의 내부 알림 시스템이 프록시 상태의 변경을 감지하면, 그 즉시 새로운 스냅샷을 생성한다. 이 새로운 스냅샷은 이전 스냅샷과는 다른 메모리 주소를 가지는, 완전히 새로운 객체다. Valtio는 이 '새로운 스냅샷'을 React 컴포넌트에게 전달한다.
React는 이전 스냅샷과 새로운 스냅샷의 참조가 다른 것을 보고, 마침내 상태 변경을 인지하고 화면을 다시 그린다!
이 방식이라면 두 마리 토끼를 모두 잡을 수 있었다.
- 개발자는 여전히 원본 프록시 상태(
state
)를 직접, 직관적으로 변경할 수 있다. (Valtio의 핵심 가치 유지) - React 컴포넌트는 항상 최신의 '불변 스냅샷'을 받아서 사용한다. (React의 불변성 요구사항 충족 및 효율적인 렌더링 가능)
"변경은 원본 프록시에 쉽게, 사용은 불변 스냅샷으로 안전하게!"
카토의 눈빛이 다시 빛나기 시작했다. 막혔던 혈관이 뚫린 듯, 아이디어가 샘솟았다. 불변 스냅샷. 이것이야말로 가변적인 Proxy 상태와 불변성을 요구하는 React 사이를 이어주는 완벽한 다리가 될 수 있었다.
첫 번째 기술적 난관은 이제 해결의 실마리를 찾았다. 남은 것은 이 '불변 스냅샷'을 어떻게 효율적으로 생성하고, 어떻게 React 컴포넌트에게 자연스럽게 전달할 것인가 하는 문제였다. 그의 시선은 이미 다음 단계, 스냅샷을 구독하고 사용하는 React 훅(Hook)의 설계를 향하고 있었다. Valtio의 진정한 여정은 이제부터 시작이었다.