JotaiJotai

상태
React를 위한 기본적이고 유연한 상태 관리

Effect

jotai-effect는 반응형 사이드 이펙트를 위한 유틸리티 패키지입니다.

설치

npm install jotai-effect

atomEffect

atomEffect는 Jotai에서 사이드 이펙트를 선언하고 아톰을 동기화하기 위한 유틸리티 함수입니다. 상태 변화를 관찰하고 반응하는 데 유용합니다.

파라미터

type CleanupFn = () => void
type EffectFn = (
get: Getter & { peek: Getter },
set: Setter & { recurse: Setter },
) => CleanupFn | void
declare function atomEffect(effectFn: EffectFn): Atom<void>

effectFn (필수): get을 통해 상태 업데이트를 감지하고 set을 통해 상태 업데이트를 작성하는 함수입니다. effectFn은 다른 Jotai 아톰과 상호작용하는 사이드 이펙트를 생성할 때 유용합니다. 클린업 함수를 반환하여 이러한 사이드 이펙트를 정리할 수 있습니다.

사용법

Atom 변경 사항 구독하기

import { atomEffect } from 'jotai-effect'
const loggingEffect = atomEffect((get, set) => {
// 마운트 시 또는 someAtom이 변경될 때마다 실행됨
const value = get(someAtom)
loggingService.setValue(value)
})

사이드 이펙트 설정 및 해제

import { atomEffect } from 'jotai-effect'
const subscriptionEffect = atomEffect((get, set) => {
const unsubscribe = subscribe((value) => {
set(valueAtom, value)
})
return unsubscribe
})

아톰 또는 훅으로 마운팅하기

atomEffect를 사용해 효과를 정의한 후, 다른 아톰의 read 함수 내부에 통합하거나 Jotai 훅에 전달할 수 있습니다.

const anAtom = atom((get) => {
// anAtom이 마운트될 때 atomEffect를 마운트
get(loggingEffect)
// ...
})
// 컴포넌트가 마운트될 때 atomEffect를 마운트
function MyComponent() {
useAtom(subscriptionEffect)
// ...
}

atomEffect 동작

  • 클린업 함수: 클린업 함수는 컴포넌트가 언마운트되거나 재평가되기 전에 호출됩니다.

    예제
    atomEffect((get, set) => {
    const intervalId = setInterval(() => set(clockAtom, Date.now()))
    return () => clearInterval(intervalId)
    })
  • 무한 루프 방지: atomEffectset을 통해 감시 중인 값을 변경해도 다시 실행되지 않습니다.

    예제
    const countAtom = atom(0)
    atomEffect((get, set) => {
    // 이 코드는 무한 루프를 발생시키지 않음
    get(countAtom) // 마운트 후, count는 1이 됨
    set(countAtom, increment)
    })
  • 재귀 지원: set.recurse를 사용하면 동기 및 비동기 상황에서 재귀를 지원합니다. 단, 클린업 함수에서는 재귀를 지원하지 않습니다.

    예제
    const countAtom = atom(0)
    atomEffect((get, set) => {
    // 1초마다 count를 증가시킴
    const count = get(countAtom)
    const timeoutId = setTimeout(() => {
    set.recurse(countAtom, increment)
    }, 1000)
    return () => clearTimeout(timeoutId)
    })
  • Peek 지원: get.peek을 사용하면 변경 사항을 구독하지 않고도 atom 데이터를 읽을 수 있습니다.

    예제
    const countAtom = atom(0)
    atomEffect((get, set) => {
    // countAtom이 변경되어도 다시 실행되지 않음
    const count = get.peek(countAtom)
    })
  • 다음 마이크로태스크에서 실행: effectFn은 모든 Jotai 동기 읽기 평가가 완료된 후, 다음 사용 가능한 마이크로태스크에서 실행됩니다.

    예제
    const countAtom = atom(0)
    const logAtom = atom([])
    const logCounts = atomEffect((get, set) => {
    set(logAtom, (curr) => [...curr, get(countAtom)])
    })
    const setCountAndReadLog = atom(null, async (get, set) => {
    get(logAtom) // [0]
    set(countAtom, increment) // 다음 마이크로태스크에서 effect 실행
    get(logAtom) // [0]
    await Promise.resolve().then()
    get(logAtom) // [0, 1]
    })
    store.set(setCountAndReadLog)
  • 동기 업데이트 배치 처리 (원자적 트랜잭션): atomEffect atom 의존성에 대한 여러 동기 업데이트는 배치 처리됩니다. 이펙트는 최종 값으로 단일 원자적 트랜잭션으로 실행됩니다.

    예제
    const enabledAtom = atom(false)
    const countAtom = atom(0)
    const updateEnabledAndCount = atom(null, (get, set) => {
    set(enabledAtom, (value) => !value)
    set(countAtom, (value) => value + 1)
    })
    const combos = atom([])
    const combosEffect = atomEffect((get, set) => {
    set(combos, (arr) => [...arr, [get(enabledAtom), get(countAtom)]])
    })
    store.set(updateEnabledAndCount)
    store.get(combos) // [[false, 0], [true, 1]]
  • 조건부 atomEffect 실행: atomEffect는 애플리케이션 내에서 마운트된 경우에만 활성화됩니다. 이는 필요하지 않을 때 불필요한 계산과 사이드 이펙트를 방지합니다. 언마운트를 통해 이펙트를 비활성화할 수 있습니다.

    예제
    atom((get) => {
    if (get(isEnabledAtom)) {
    get(effectAtom)
    }
    })
  • 멱등성: atomEffect는 상태가 변경될 때 한 번만 실행되며, 몇 번 마운트되든 상관없습니다.

    예제
    let i = 0
    const effectAtom = atomEffect(() => {
    get(countAtom)
    i++
    })
    const mountTwice = atom(() => {
    get(effectAtom)
    get(effectAtom)
    })
    store.set(countAtom, increment)
    Promise.resolve.then(() => {
    console.log(i) // 1
    })

의존성 관리

마운트 이벤트 외에도, 이펙트는 의존성 중 하나라도 값이 변경될 때 실행됩니다.

  • 동기(Sync): 이펙트의 동기 평가 중에 get으로 접근한 모든 아톰은 아톰의 내부 의존성 맵에 추가됩니다.

    예제
    atomEffect((get, set) => {
    // `anAtom`이 변경될 때마다 업데이트되지만, `anotherAtom`이 변경될 때는 업데이트되지 않음
    get(anAtom)
    setTimeout(() => {
    get(anotherAtom)
    }, 5000)
    })
  • 비동기(Async): 비동기 이펙트의 경우, 보류 중인 fetch 요청과 Promise를 취소하기 위해 abort controller를 사용해야 합니다.

    예제
    atomEffect((get, set) => {
    const count = get(countAtom) // countAtom은 아톰 의존성
    const abortController = new AbortController()
    ;(async () => {
    try {
    await delay(1000)
    abortController.signal.throwIfAborted()
    get(dataAtom) // dataAtom은 아톰 의존성이 아님
    } catch (e) {
    if (e instanceof AbortError) {
    // 비동기 정리 로직
    } else {
    console.error(e)
    }
    }
    })()
    return () => {
    // countAtom이 변경될 때 abort
    abortController.abort(new AbortError())
    }
    })
  • 정리(Cleanup): 정리 함수에서 get으로 아톰에 접근해도 아톰의 내부 의존성 맵에 추가되지 않습니다.

    예제
    atomEffect((get, set) => {
    // 마운트 시 한 번 실행
    // `idAtom`이 변경되어도 업데이트되지 않음
    const unsubscribe = subscribe((valueAtom) => {
    const value = get(valueAtom)
    // ...
    })
    return () => {
    const id = get(idAtom)
    unsubscribe(id)
    }
    })
  • 의존성 맵 재계산: 의존성 맵은 매 실행마다 재계산됩니다. 현재 실행 중에 감시되지 않은 아톰은 현재 실행의 의존성 맵에 포함되지 않습니다. 활발히 감시되는 아톰만 의존성으로 간주됩니다.

    예제
    const isEnabledAtom = atom(true)
    atomEffect((get, set) => {
    // `isEnabledAtom`이 true면, `isEnabledAtom` 또는 `anAtom`이 변경될 때 실행
    // 그렇지 않으면 `isEnabledAtom` 또는 `anotherAtom`이 변경될 때 실행
    if (get(isEnabledAtom)) {
    const aValue = get(anAtom)
    } else {
    const anotherValue = get(anotherAtom)
    }
    })

withAtomEffect

withAtomEffect는 대상 아톰의 복제본에 이펙트를 바인딩합니다. 이는 대상 아톰의 복제본이 마운트될 때 활성화되는 이펙트를 생성하는 데 유용합니다.

Parameters_3PmHDWfvMww8aM5M8X9jZM

declare function withAtomEffect<T>(
targetAtom: Atom<T>,
effectFn: EffectFn,
): Atom<T>

targetAtom (필수): 이펙트가 바인딩될 아톰.

effectFn (필수): get을 통해 상태 업데이트를 감지하고, set을 통해 상태 업데이트를 작성하는 함수.

반환값: 대상 아톰과 동일하지만 바인딩된 이펙트를 가진 아톰.

Usage_5M9S4SS2qJNBZop3dfCjZ3

import { withAtomEffect } from 'jotai-effect'
const valuesAtom = withAtomEffect(atom(null), (get, set) => {
// valuesAtom이 마운트될 때 실행됨
const unsubscribe = subscribe((value) => {
set(valuesAtom, value)
})
return unsubscribe
})

Comparison with useEffect

컴포넌트 사이드 이펙트

useEffect는 컴포넌트를 외부 시스템과 동기화할 수 있게 해주는 React 훅입니다.

훅은 함수 컴포넌트에서 React의 상태와 생명주기 기능에 "연결"할 수 있게 해주는 함수입니다. 훅은 상태 관련 로직을 재사용할 수 있게 해주지만, 중앙 집중화하지는 않습니다. 각 훅 호출은 완전히 독립된 상태를 가집니다. 이러한 독립성을 컴포넌트 스코프라고 부를 수 있습니다. 컴포넌트의 props와 상태를 Jotai 아톰과 동기화하려면 useEffect 훅을 사용해야 합니다.

전역 사이드 이펙트

전역 사이드 이펙트를 설정할 때, useEffectatomEffect 중 어떤 것을 사용할지는 개발자의 선호에 따라 결정됩니다.
이 로직을 컴포넌트에 직접 작성할지, 아니면 Jotai 상태 모델에 작성할지는 여러분이 채택한 멘탈 모델에 따라 달라집니다.

atomEffect는 아톰 내부의 동작을 모델링하는 데 더 적합합니다.
이것은 컴포넌트가 아닌 스토어 컨텍스트에 범위가 한정됩니다.
이를 통해, 아무리 많은 호출이 발생하더라도 단일 이펙트가 사용된다는 것을 보장할 수 있습니다.

useEffect 훅을 사용할 때도, useEffect가 멱등성을 보장하도록 하면 동일한 보장을 얻을 수 있습니다.

atomEffect는 몇 가지 다른 점에서 useEffect와 구별됩니다.
아톰 상태 변경에 직접 반응할 수 있고, 무한 루프에 강하며, 조건부로 마운트될 수 있습니다.

선택은 여러분에게 달려 있습니다

useEffect와 atomEffect 모두 각각의 장점과 활용 사례가 있습니다. 여러분의 프로젝트 요구사항과 편의에 따라 적절한 방식을 선택하세요. 더 부드럽고 직관적인 개발 경험을 제공하는 접근 방식을 선택하는 것이 좋습니다. 즐거운 코딩 되세요!