- 파생 상태(Derived State) 생성
- 하나 이상의 atom/상태로부터 새로운 계산된 값을 만듦
- 기존 상태를 기반으로 복잡한 로직을 통해 새로운 상태를 생성
- 상태 추상화 및 변환
// 예시: 사용자 목록에서 성인만 필터링
const adultUsersSelector = atom((get) => {
const users = get(usersAtom);
return users.filter((user) => user.age >= 18);
});
- 성능 최적화
- 메모이제이션을 통해 불필요한 재계산을 방지
- 의존하는 atom의 값이 변경되지 않았다면 이전 계산 결과를 재사용
- 비동기 상태 처리
const userDataSelector = atom(async (get) => {
const userId = get(userIdAtom);
const response = await fetchUserData(userId);
return response.data;
});
- 복잡한 상태 로직 캡슐화
- 상태 계산 로직을 중앙집중적으로 관리 가능
- 컴포넌트에서 직접적인 계산 로직을 분리 가능
- 읽기/쓰기 selector
const temperatureSelector = atom(
(get) => {
// 읽기: 섭씨를 화씨로 변환
const celsius = get(celsiusAtom);
return (celsius _ 9/5) + 32;
},
(get, set, fahrenheit) => {
// 쓰기: 화씨를 섭씨로 변환하여 설정
const celsius = (fahrenheit - 32) _ 5/9;
set(celsiusAtom, celsius);
}
);
-
공통점
- 상태 관리의 복잡성을 추상화
- 계산된 상태의 효율적인 관리
- 반응형 상태 업데이트(특정 상태가 변경될 때 자동으로 연관된 다른 상태들이 업데이트되는 메커니즘)
-
차이점
- Recoil: 더 복잡한 의존성 추적, React Suspense와 통합되어 비동기 상태를 동기적으로 관리할 수 있도록 함
- Jotai: 더 간결하고 최소한의 API, 성능에 최적화
결론적으로 selector는 상태 관리에서 "계산된 상태"를 만들고 관리하는 강력한 추상화 메커니즘!
Jotai의 selector는 기본적으로 atom 함수의 getter 함수를 통해 구현된다
function atom(read, write?) {
return {
read: (get) => {
// 다른 atom의 값을 읽어올 수 있는 get 함수
// 의존성 추적 및 메모이제이션 로직 포함
},
write: (get, set, value) => {
// 쓰기 작업 구현 (선택적)
},
};
}
Jotai는 내부적으로 "dependency tracking" 시스템을 구현하고 있다.
selector가 다른 atom을 읽을 때마다 해당 의존성을 자동으로 추적한다.
의존하는 atom의 값이 변경되면 selector도 자동으로 다시 계산된다.
function createMemoizedSelector(computeFn) {
let lastDependencies = null;
let lastResult = null;
return (get) => {
// 현재 의존하는 atom들의 값 추적
const currentDependencies = trackDependencies(get);
// 의존성이 변경되지 않았다면 캐시된 결과 반환
if (lastDependencies && areEqual(lastDependencies, currentDependencies)) {
return lastResult;
}
// 새로운 결과 계산
const newResult = computeFn(get);
// 상태 업데이트
lastDependencies = currentDependencies;
lastResult = newResult;
return newResult;
};
}
const asyncSelector = atom(async (get) => {
// 비동기 작업 처리 메커니즘
const dependencyValue = get(someAtom);
const result = await fetchSomeData(dependencyValue);
return result;
});
- 성능 최적화, 다양한 edge case 처리 그리고 React의 동시성 모드와도 호환되도록 설계해야함
내부 구현의 핵심은 "최소한의 재계산"과 "효율적인 의존성 관리"이다.
function selector({
key, // 고유 식별자
get, // 읽기 함수
set? // 선택적 쓰기 함수
}) {
// selector의 핵심 로직
}
function createSelector(config) {
// 의존성 추적을 위한 내부 로직
const dependencyMap = new Map();
function trackDependencies(get) {
// 현재 selector가 읽는 atom들을 추적
const dependencies = [];
// 프록시를 통한 의존성 추적
const proxyGet = new Proxy(get, {
apply: (target, thisArg, args) => {
const [atom] = args;
dependencies.push(atom);
return target(...args);
},
});
// selector의 계산 로직 실행
config.get(proxyGet);
return dependencies;
}
}
function createMemoizedSelector(config) {
let lastDependencies = null;
let lastResult = null;
return {
read: (get) => {
// 현재 의존성 추적
const currentDependencies = trackDependencies(get);
// 의존성 변경 여부 확인
const dependenciesChanged =
!lastDependencies ||
currentDependencies.some(
(dep, index) => dep !== lastDependencies[index]
);
// 캐시된 결과 또는 새로운 결과 반환
if (!dependenciesChanged) {
return lastResult;
}
// 새로운 결과 계산
const newResult = config.get(get);
// 상태 업데이트
lastDependencies = currentDependencies;
lastResult = newResult;
return newResult;
},
};
}
function asyncSelector({
key,
get: async (get) => {
// 비동기 작업 처리
// Recoil은 내부적으로 Suspense와 연동
const dependencyValue = get(someAtom);
const result = await fetchData(dependencyValue);
return result;
}
})
- 프록시를 통한 정교한 의존성 추적
- 세밀한 메모이제이션
- 비동기 상태 관리의 복잡성 숨기기