티스토리 뷰
React에서 상태를 관리할 때 항상 setState를 직접 호출해야 값이 변경된다.
React는 불변성(immutability)을 기본 철학으로 삼고 있으며, 명시적으로 상태를 변경하는 구조가 핵심 이기 때문이다.
하지만 Vue.js를 써본 사람이라면, Vue(3)의 ref()나 reactive()처럼 값을 직접 변경하면 반응적으로 UI가 갱신되는 구조가 더 직관적이고 편리하게 느껴질 수도 있다.
예를 들어, Vue(3)에서는 아래처럼 쓸 수 있다.
const state = reactive({ count: 0 });
state.count++; // 자동으로 반응!
React에서도 이런 방식으로 상태를 관리해보고 싶다는 생각이 들어,
Proxy 객체를 활용해 Vue처럼 동작하는 useReactive 훅을 구현해 보았다.
구현 아이디어
React에서는 다음과 같은 방식으로 반응형 객체를 구현하기로 하였다
- Proxy를 사용해 객체의 프로퍼티 변경을 감지
- 상태 객체는 useRef를 사용해 유지하고, ref 객체에 Proxy 프러퍼티를 추가하고 해당 객체를 return 해주어 proxy 객체의 중복 생성 방지
- 감지된 변경이 있을 경우, useState로 강제 리렌더링을 유도 (useState 에 proxy 객체를 직접 저장시 state에 proxy가 직접 추가 된다는 단점이 있다)
Steps
1. useRef를 사용해 current value를 생성
const reactiveRef: ReactiveRef<T> = useRef<ReactiveValue<T>>
((initialState instanceof Object ? initialState : { value: initialState }) as ReactiveValue<T> );
InitialState를 분기 처리한 이유
객체의 원본 상태를 보존하기 위해 useRef를 사용한다.
여기서 vue의 ref와 reactive처럼 원시값과 객체/배열을 구분해야 한다.
<Vue에서 ref와 reactive의 차이>
더보기
더보기
더보기
Vue에서는 반응형 상태를 만들 때 ref()와 reactive() 두 가지 방법이 있다.
둘 다 값의 변화를 감지하여 UI를 업데이트할 수 있도록 하지만, 동작 방식에 차이가 있다.
1. ref()
- **원시 값(primitive values)**과 객체를 모두 감쌀 수 있다.
- 원시 값은 { value: T } 형태로 감싸야 반응형이 된다.
- 객체를 넘겨도 { value: 객체 } 형태가 아닌, reactive()와 비슷한 Proxy 객체로 변환된다.
2. reactive()
- 객체(Object) 타입만 반응형으로 만들 수 있다.
- ref()와 달리 .value를 사용하지 않고 바로 속성에 접근한다.
차이 정리
기능ref()reactive()
지원하는 타입 | 모든 값(원시 값 + 객체) | 객체만 가능 |
원시 값 다루기 | value 속성 필요 | 불가능 |
객체 다루기 | Proxy 기반(내부적으로 reactive 사용) | Proxy 기반 |
구조 분해 가능 여부 | const { value } = ref(0); 불가능 | const { count } = reactive({ count: 0 }); 가능 |
- reactive()는 객체만 처리할 수 있음
- Vue에서는 reactive(0)처럼 원시 값을 넘기면 에러가 발생한다
- React의 Proxy를 그대로 사용한다면 object가 아닌 경우 예외가 발생할 가능성이 있다.
- ref()의 value 패턴을 도입
- reactive()는 객체에서만 동작하므로, React에서도 initialState가 객체라면 그대로 사용한다
- 하지만 원시 값(숫자, 문자열 등)이 들어오면 { value: 값 } 형태로 감싸 준다.
- 이는 Vue에서 ref(0)이 { value: 0 } 형태로 만들어지는 것과 동일한 처리 방식.
- React에서 활용
const state1 = useReactive({ count: 0 }); // 객체 → 그대로 사용
console.log(state1.count); // OK
const state2 = useReactive(0); // 원시 값 → { value: 0 } 로 감싸기
console.log(state2.value); // OK
2. 렌더링 트리거: useState 사용
const [, forceRender] = useState(0);
- 값이 바뀔 때 React가 다시 렌더링되도록 강제로 상태를 업데이트합니다.
3. 값 변경 감지 로직: Proxy 사용
// Proxy는 최초 1회만 생성
if (reactiveRef.current && !reactiveRef.proxy) {
reactiveRef.proxy = new Proxy(reactiveRef.current, {
get(target, key) {
return target[key as keyof typeof target];
},
set(target, key, value) {
// 값 변경을 확인한다. 이 로직이 없으면 무한 재귀 호출이 발생한다.
if (target[key as keyof typeof target] === value) return true;
target[key as keyof typeof target] = value;
forceRender((prev) => prev + 1);
return true;
},
});
}
- 한 번만 Proxy를 생성하여 메모리 낭비를 방지합니다.
구현 코드
import { useState, useRef, MutableRefObject } from "react";
// 리액티브 상태 타입 정의
type ReactiveValue<T> = T extends object ? T : { value: T };
// 리액티브 상태 Ref 타입
type BaseRef<T> = MutableRefObject<ReactiveValue<T>>;
interface ReactiveRef<T> extends BaseRef<T> {
proxy?: ReactiveValue<T>;
}
// useReactive 훅
export const useReactive = <T>(initialState: T): ReactiveValue<T> => {
const [, forceRender] = useState(0); // 변경 알림용 dummy state
const reactiveRef: ReactiveRef<T> = useRef<ReactiveValue<T>>(
(initialState instanceof Object
? initialState
: { value: initialState }) as ReactiveValue<T>
);
// Proxy는 최초 1회만 생성
if (reactiveRef.current && !reactiveRef.proxy) {
reactiveRef.proxy = new Proxy(reactiveRef.current, {
get(target, key) {
return target[key as keyof typeof target];
},
set(target, key, value) {
if (target[key as keyof typeof target] === value) return true;
target[key as keyof typeof target] = value;
forceRender((prev) => prev + 1);
return true;
},
});
}
return reactiveRef.proxy!;
};
사용 예시
const Counter = () => { const state = useReactive({ count: 0 });
return ( <div> <p>count: {state.count}</p> <button onClick={() => state.count++}>+</button> </div> ); };
Vue의 reactive와 거의 비슷한 사용법으로 작성할 수 있다!
결과
한계점 및 고려사항
- 양방향 바인딩(v-model) 은 구현 불가
- Vue의 v-model과 같은 양방향 데이터 바인딩은 템플릿 컴파일 단계에서 지원되므로 React에서는 구현이 어렵다
- 예를 들어 input 바인딩은 여전히 onChange로 해결해야 한다
- React의 의존성 추적 시스템과는 다름
- Proxy 객체는 useEffect, useMemo 등의 의존성 배열에서 변경 감지가 되지 않는다.
- React에 꼭 필요한가?
- React는 불변성(immutability)을 기본 철학으로 삼고 있으며, 명시적으로 상태를 변경하는 구조가 핵심이다!
- Vue의 패러다임을 억지로 React에 끼워 맞추는 건, 경우에 따라 오히려 유지보수에 불리할 수도 있다. 그러므로 react에서의 실용성은 떨어진다고 볼 수 있다.
'프론트엔드' 카테고리의 다른 글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- react fiber 3d
- ts glsl
- Vue.js
- vue ref
- leva
- eslint
- 394 decode string
- react 3d
- react 3d animation
- react glsl
- react 3d text
- attempted import error: bvh_struct_definitions' is not exported from './gpu/bvhshaderglsl.js' (imported as 'bvhshaderglsl').
- vue
- react three fiber
- [leetcode] 394. decode string js
- vue react
- 394. decode string javascript
- typescript gsls
- react 3d 에니메이션
- vue reactive
- next.js import glsl
- [leetcode] 394. decode string
- react ref reative
- react three fiber leva
- react vue
- three.js leva
- vue3
- webpack glsl
- 394. decode string js
- react leva
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
글 보관함