Flutter 프로젝트에서 상태관리 도구를 사용하셨던데 어떤 도구를 사용하셨고, 상태관리가 왜 필요한가요?
→ Flutter 프로젝트를 진행하면서 Getx 라는 전역 상태 툴을 활용하였습니다. 기본적으로 상태에 대한 UI의 변화는 StatefulWidget 내부에서 수행되지만, 여러서 위젯 내부에서 상태 변화를 고려해야하는 경우, Getx의 Rx 변수를 선언하여 전역 상태관리를 할 수 있습니다. 그리고 Rx 변수들은 controller로 선언된 함수들에 의해서 상태를 변화시키고 관리할 수 있습니다.
React 프로젝트에서도 전역 상태 관리 도구를 사용하셨는데, 두 도구는 어떤 차이가 있었나요?
→ React 프로젝트에서는 Zustand를 사용해 전역 상태 관리를 했습니다. React에서는 컴포넌트 간에 상태를 전달할 때 props drilling 문제가 발생할 수 있는데, 이 문제를 해결하기 위해 Store라는 중앙 저장소 개념을 도입한 상태 관리 도구들을 사용합니다.
→ Zustand는 중앙 집중식 Store를 사용하여 여러 컴포넌트가 공유하는 상태와 이를 조작하는 action들을 한 곳에 모아 관리합니다. 반면, GetX는 특정 상태를 전역으로 선언한 Rx 변수와 Controller를 통해 필요할 때마다 각 컴포넌트에서 상태를 관리하고 반응형으로 UI를 업데이트하는 방식입니다. Zustand는 중앙 집중화된 관리 방식인 반면, GetX는 보다 분산된 방식으로 각각의 컴포넌트에서 필요한 상태만 관리하는 구조입니다.
useEffect 훅의 타입 정의는 아래와 같다.
function useEffect(effect: EffectCallBack, deps?: DependencyList): void;
type DependencyList = ReadonlyArray<any>;
type EffectCallback = () => void | Destructor;
useEffect에서 EffectCallBack 함수 타입을 가지는 effect 인자는 아무것도 반환하지 않거나 Destructor를 반환한다고 한다.
만약 effectCallBack 함수가 비동기로 쓰이게 되면, 콜백함수는 Promise를 return하게 된다. 기본적으로 Promise 타입의 함수는 반환할 수 없고, 허용했을 때 race condition의 가능성 때문에 동기 함수를 사용하는 것을 원칙으로 정했다.
만약 내부적으로 비동기로직을 활용해야한다면 아래와 같이 비동기 함수를 호출하여 useEffect 콜백 자체는 동기를 유지할 수 있도록 해야한다.
useEffect(() => {
const fetchAsync = async () => {
...
const res = await someAsyncFunction();
...
}
fetchAsync();
}, []}
useEffect(() => {
...
return () => {}
}, []);
useEffect 내부에서 함수를 return 할 수 있다. 이 함수는 컴포넌트 해제 전에 정리 작업을 수행하기 위한 함수로, 컴포넌트가 마운트 해제될 때만 실행된다고만 알고 있었었다. 의존성 배열이 존재하면, 의존서 배열의 값이 변경 될 때마다 실행된다고 한다.
useRef의 제네릭에 몇가지 다양한 케이스가 들어갈 수 있는데 그 중 내가 자주 쓰는 두 표현식에 어떤 차이가 있는지 알게되어서 정리를 한다.
아래 두 방식인데,
const componentRef1 = useRef<HTMLDivElement | null>()
const componentRef2 = useRef<HTMLDivElement>()
내가 사용할 당시에는 단순하게 ref가 참조하는 값에 대해서 null을 고려하는지 여부에 따라 사용법을 구분했었다. 하지만 두 방식에는 내가 생각하지 못한 차이가 있었다.
실제 useRef를 정의한 코드를 보면 여러 형태 중 아래와 같은 두가지 형태를 볼 수 있다.
function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T>;
눈에 띄는 차이는 return하는 객체의 타입이 다르다는 점이다.
만약 제네릭에 HTMLDivElement | null
타입을 사용한 경우 useRef는 첫번째 케이스에 해당하고 MutableRefObject
타입을 따른다.
Mutable하다는 단어에 맞게 해당 타입은 current를 변경할 수 있는 값이 되고, ref.current의 값을 조작하거나 바뀔 수 있다.
RefObject
는 반대로 current의 값을 임의로 조작할 수 없다.
처음 이부분을 알게 되었을 때, current 자체를 바꿀 수 있다. or 없다. 로 구분했었다. 하지만 그것보다 제네릭의 타입에 의해서 리턴하는 객체의 차이가 생긴 것 같다는 생각을 하게 되었다. 위의 상단의 예시에서도
HTMLDivElement | null
을 제네릭으로 넘겨주는 경우 useRef는 두 타입 중 하나의 타입에 해당하는 값을 가지게 되기 때문에 어느 한 쪽으로변경이 가능해야
할 것이다. 반대로HTMLDivElement
단일 타입으로 선언한 경우 해당 타입 외에 다른 타입이 할당 되지 않을 것이기 때문에ReadOnly
인RefObject
로 리턴해준 것 같다.
useRef의 초기 값을 null로 설정하는 이유
useRef
에서 초기값을 null
로 설정하는 이유는 주로 초기 렌더링 시점에는 참조할 DOM 요소가 없기 때문에 이를 명시적으로 표현하고, 타입 안정성을 유지하며, 이후 렌더링 완료 후 DOM 요소를 참조하도록 하기 위함이라고 한다.useRef를 자식 컴포넌트 뿐만 아니라 다른 값을 저장해서 사용할 수 도 있다.
const isLoading = useRef(false);
일반적으로 컴포넌트 내부에서 useState를 통한 상태를 활용하는데, state는 일반적으로 변경 함수 호출 이후 랜더링이 완료된 시점에서 변경된 값을 확인할 수 있다.
반면 useRef의 경우 값이 변경된 즉시 변경된 값으로 확인가능 하다.
랜더링에 영향을 주지않는 값이고, 즉시 변경 여부를 확인해야하는 경우에 useState 대신 useRef를 써보는 것도 좋을 것 같다.