[Core] - atom

강 진성·2024년 3월 24일
0

Jotai

목록 보기
2/5

Atom


Atom 기능은 Atom 구성을 생성하는 것입니다. 우리는 이를 "atom config(아톰 설정)"라고 부릅니다. 이는 단지 정의일 뿐이고 아직 값을 보유하지 않기 때문입니다. 문맥이 명확하다면 그냥 "atom"이라고 부를 수도 있습니다.

Atom 구성은 변경할 수 없는 객체입니다. Atom 구성 개체에는 값이 없습니다. 원자 값은 저장소에 존재합니다.

기본 원자(config)를 생성하려면 초기 값을 제공하기만 하면 됩니다.

import { atom } from 'jotai'

const priceAtom = atom(10)
const messageAtom = atom('hello')
const productAtom = atom({ id: 12, name: 'good stuff' })

파생 원자를 생성할 수도 있습니다. 세 가지 패턴이 있습니다.

  • Read - only atom (읽기 전용 원자)
  • Write - only atom (쓰기 전용 원자)
  • Read - Write atom (읽기 쓰기 원자)

파생된 원자를 생성하기 위해 읽기 함수와 선택적인 쓰기 함수를 전달합니다.

// 읽기 전용 아톰
const readOnlyAtom = atom((get) => get(priceAtom) * 2)

// 쓰기 전용 아톰
const writeOnlyAtom = atom(
  null, // 첫 번째 인수에 `null`을 전달하는 것이 관례입니다. (읽기 기능이 없으므로)
  (get, set, update) => {
    // `update`는 이 Atom을 업데이트하기 위해 받는 단일 값입니다.
    set(priceAtom, get(priceAtom) - update.discount)
    // 또는 함수가 호출될 때,
    // 두 번째 매개변수로 함수를 전달하여
	  // 원자의 현재 값을 첫 번째 매개변수로 받을 수 있습니다.
    set(priceAtom, (price) => price - update.discount)
  },
)

// 읽기 쓰기 아톰
const readWriteAtom = atom(
  (get) => get(priceAtom) * 2,
  (get, set, newPrice) => {
    set(priceAtom, newPrice / 2)
    // 동시에 원하는 만큼 많은 원자를 설정할 수 있습니다.
  },
)

읽기 기능을 사용하면 atom 값을 읽는 것입니다. 반응형이며 읽기 종속성이 추적됩니다

쓰기 함수에 들어가는 것도 atom 값을 읽는 것이지만 추적되지는 않습니다. 또한 Jotai v1 API에서는 해결되지 않은 비동기 값을 읽을 수 없습니다.

쓰기 기능에 설정된 것은 atom 값을 쓰는 것입니다. 대상 원자의 쓰기 기능을 호출합니다.


[궁금한점] 그래서 이게 무슨 말이야?

1. 읽기 전용 아톰? 쓰기 전용 아톰?

  • 읽기전용(Read-only) 아톰 (readOnlyAtom)
    이 아톰은 다른 아톰의 상태를 기반으로 값을 계산하지만, 자체적으로는 값을 변경할 수 없습니다. readOnlyAtompriceAtom의 값을 가져와서 2를 곱한 값을 반환합니다. 이 아톰은 값을 읽을 수만 있고, 직접적으로 값을 수정할 수는 없습니다.

  • 쓰기전용(Write-only) 아톰 (writeOnlyAtom)
    이 아톰은 값을 읽을 수 없고, 오직 다른 아톰의 값을 변경하는 데만 사용됩니다.
    writeOnlyAtomupdate 객체의 discount 속성을 사용하여 priceAtom의 값을 감소시킵니다.
    첫 번째 set 호출은 현재 priceAtom의 값에서 discount를 빼는 반면, 두 번째 set 호출은 priceAtom의 현재 값을 함수의 인자로 받아 동일한 연산을 수행합니다.

2. 그래서 이거 어떻게 쓰는건데?

알고봤더니 말만 거창한거였다. 읽기 전용? 쓰기 전용? 읽기쓰기아톰? 그런거 다 필요없다.

useState 쓸 줄만 알면 useAtom도 쓰기 쉽다.

[읽기 전용 아톰]

import { useAtom } from 'jotai';
import { readOnlyAtom } from './store'; // 가정한 경로

function ReadOnlyComponent() {
  const [readOnlyValue] = useAtom(readOnlyAtom); // 값을 읽음

  return <div>계산된 가격: {readOnlyValue}</div>;
}

[쓰기 전용 아톰]

import { useAtom } from 'jotai';
import { writeOnlyAtom } from './store'; // 가정한 경로

function WriteOnlyComponent() {
  const [, updatePrice] = useAtom(writeOnlyAtom); // 업데이트 함수를 받음

  const applyDiscount = () => {
    updatePrice({ discount: 10 }); // 할인 적용
  };

  return <button onClick={applyDiscount}>할인 적용하기</button>;
}

[읽기 쓰기 아톰]

import { useAtom } from 'jotai';
import { readWriteAtom } from './store'; // 가정한 경로

function ReadWriteComponent() {
  const [price, setPrice] = useAtom(readWriteAtom); // 값을 읽고 업데이트 함수를 받음

  const updatePrice = (newPrice) => {
    setPrice(newPrice); // 새 가격으로 업데이트
  };

  return (
    <div>
      현재 가격: {price}
      <button onClick={() => updatePrice(price + 10)}>가격 올리기</button>
    </div>
  );
}

useState 랑 무슨 차이가 있는가? 쓰는 방법에 아무 차이가 없다. 상태관리를 전역으로 관리하냐 안하냐의 차이일 뿐이다.



Note about creating an atom in render function


Atom 구성은 어디에서나 생성될 수 있지만 참조 평등이 중요합니다. 동적으로 생성될 수도 있습니다. 렌더링 함수에서 원자를 생성하려면 안정적인 참조를 얻기 위해 useMemo 또는 useRef가 필요합니다. 메모를 위해 useMemo나 useRef를 사용하는 것이 의심스러우면 useMemo를 사용하세요. 그렇지 않으면 useAtom에서 무한 루프가 발생할 수 있습니다.

const Component = ({ value }) => {
  const valueAtom = useMemo(() => atom({ value }), [value])
  // ...
}

[궁금한점] 그래서 이게 무슨 말이야?

1. 이 문장이 의미하는 바

  • 참조 평등
    아톰은 참조 평등이 보장되어야 하므로, 동일한 아톰에 대해 여러 번 접근하더라도 매번 같은 객체를 참조해야 합니다. 이는 React가 아톰을 효율적으로 구독하고 업데이트하기 위해 필요합니다.

  • 동적 생성
    아톰은 일반적으로 전역적으로 선언되거나 컴포넌트 외부에서 선언됩니다. 하지만 때로는 동적인 조건에 따라 아톰을 생성해야 할 수도 있습니다.

  • useMemo나 useRef
    컴포넌트 내부에서 아톰을 생성할 때는 useMemo 또는 useRef 훅을 사용하여 아톰이 렌더링 간에 동일하게 유지되도록 보장해야 합니다. 그렇지 않으면 컴포넌트가 재렌더링될 때마다 새로운 아톰이 생성되어 무한 루프나 비효율적인 동작을 유발할 수 있습니다.

2. 사용 예시

Jotai 아톰을 컴포넌트 내부에서 동적으로 생성할 경우, 아래와 같이 useMemo를 사용하여 참조 평등을 보장할 수 있습니다.

import { atom, useAtom } from 'jotai';
import { useMemo } from 'react';

function MyComponent({ itemId }) {
  // 아이템 ID에 따라 동적으로 아톰을 생성하되, 참조를 유지합니다.
  const itemAtom = useMemo(() => atom(`value:${itemId}`), [itemId]);
  
  const [value, setValue] = useAtom(itemAtom);

  // ... 컴포넌트 로직 ...
  
  return (
    <div>
      {value}
      {/* 아톰 값 업데이트 로직 */}
    </div>
  );
}

이 예시에서 useMemoitemId가 변경될 때만 새로운 아톰을 생성하도록 보장합니다. itemId가 변경되지 않는 한, useMemo는 이전에 생성된 아톰을 재사용하여 성능을 최적화하고, 불필요한 렌더링을 방지합니다. useMemo는 의존성 배열([itemId]) 내의 값들이 변경될 때만 첫 번째 인자로 넘겨진 함수를 실행하여 새로운 값을 계산합니다.

이런 방식으로 아톰을 사용하면, 컴포넌트의 상태를 더 유연하고 효율적으로 관리할 수 있으며, 컴포넌트의 렌더링 성능을 향상시킬 수 있습니다.


  • useMemo: React의 useMemo 훅을 호출합니다.

  • () => atom(value:${itemId}): 첫 번째 매개변수는 "생성 함수(create function)"로, 메모이제이션할 값을 반환하는 함수입니다. 여기서 atom은 Jotai 라이브러리의 함수로, 새로운 아톰을 생성합니다. atom 함수에 문자열 템플릿 리터럴을 사용하여 itemId를 기반으로 고유한 식별 문자열을 만듭니다. 이 문자열은 아톰의 초기 값을 설정하는 데 사용됩니다. 이 함수는 itemId가 변경될 때마다 새로운 아톰을 생성합니다.

  • [itemId]: 두 번째 매개변수는 "의존성 배열(dependency array)"로, 배열 안의 값들이 변경될 때만 생성 함수를 다시 실행하여 새 값을 계산하도록 지시합니다. 이 경우에는 itemId가 변경될 때만 새로운 아톰을 생성하도록 설정되어 있습니다.


이 코드의 동작 원리는 다음과 같습니다:

  1. 컴포넌트가 렌더링될 때 useMemo 훅이 호출됩니다.

  2. 만약 itemId가 이전 렌더링 때와 동일하다면, React는 useMemo에 의해 저장된 값을 재사용합니다.

  3. itemId가 변경되면, useMemo는 제공된 함수를 실행하여 새로운 아톰을 생성하고, 그 값을 반환합니다.

  4. 이렇게 생성된 itemAtom은 컴포넌트에서 사용될 수 있으며, 컴포넌트가 재렌더링될 때마다 동일한 itemId에 대해서는 동일한 아톰 참조를 유지합니다.



Signatures


// 기본적인 아톰으로, 초기 값을 매개변수로 받습니다.
// 이 값은 아톰의 상태를 나타내며, 이 아톰은 읽고 쓰기가 모두 가능합니다.
function atom<Value>(initialValue: Value): PrimitiveAtom<Value>

// 읽기 전용 아톰으로, 'get'함수를 매개변수로 받는 'read' 함수를 매개변수로 받습니다.
// 이 함수는 다른 아톰의 값을 가져오는데 사용되며,
// 이 아톰은 값을 읽을 수만 있고 변경할 수는 없습니다.
function atom<Value>(read: (get: Getter) => Value): Atom<Value>

// 파생된 아톰으로, 읽기와 쓰기 둘 다 가능합니다.
// 'read' 함수는 다른 아톰의 값을 가져오는 데 사용되며,
// 'write' 함수는 아톰의 값을 설정하는데 사용됩니다.
// 'write' 함수는 'get' 함수로 현재 상태를 읽고, 'set' 함수로 다른 아톰의 상태를
// 변경할 수 있으며, 추가 매개변수 '...args'를 통해 다른 값들을 받아들일 수 있습니다.
function atom<Value, Args extends unknown[], Result>(
  read: (get: Getter) => Value,
  write: (get: Getter, set: Setter, ...args: Args) => Result,
): WritableAtom<Value, Args, Result>

// 쓰기 전용 파생 아톰으로, 'read' 에는 기본 값을 직접 제공하고, 'write' 함수를 통해
// 아톰의 값을 변경할 수 있습니다.
// 이 아톰은 상태를 읽는 것에 대신에 주로 상태를 설정하는 데 사용됩니다.
function atom<Value, Args extends unknown[], Result>(
  read: Value,
  write: (get: Getter, set: Setter, ...args: Args) => Result,
): WritableAtom<Value, Args, Result>

  • initialValue
    값이 변경될 때까지 원자가 반환할 초기 값입니다.

  • 읽기
    원자를 읽을 때마다 평가되는 함수입니다. read의 signature는 (get) => Value이며, get은 아래 설명과 같이 Atom 구성을 가져와 Provider에 저장된 값을 반환하는 함수입니다. 종속성은 추적되므로 원자에 대해 get을 한 번 이상 사용하면 원자 값이 변경될 때마다 읽기가 다시 평가됩니다.

  • 쓰기
    더 나은 설명을 위해 원자 값을 변경하는 데 주로 사용되는 함수입니다. 이는 반환된 useAtom 쌍의 두 번째 값인 useAtom()[1]을 호출할 때마다 호출됩니다. 원시 원자에서 이 함수의 기본값은 해당 원자의 값을 변경합니다. 쓰기의 signature는 (get, set, ...args) => 결과입니다. get은 위에서 설명한 것과 유사하지만 종속성을 추적하지 않습니다. set은 원자 구성과 새 값을 사용하여 공급자의 원자 값을 업데이트하는 함수입니다. ...args는 useAtom()[1]을 호출할 때 받는 인수입니다. 결과는 쓰기 함수의 반환 값입니다.


const primitiveAtom = atom(initialValue)
const derivedAtomWithRead = atom(read)
const derivedAtomWithReadWrite = atom(read, write)
const derivedAtomWithWriteOnly = atom(null, write)

Atom에는 쓰기 가능한 Atom과 읽기 전용 Atom의 두 가지 종류가 있습니다. 원시 원자는 항상 쓰기 가능합니다. 쓰기가 지정된 경우 파생 원자를 쓸 수 있습니다. 기본 원자 쓰기는 React.useState의 setState와 동일합니다.



debugLabel property


생성된 Atom 구성에는 선택적인 속성 debugLabel이 있을 수 있습니다. 디버그 레이블은 디버깅 시 원자를 표시하는 데 사용됩니다. 자세한 내용은 디버깅 가이드를 참조하세요.

참고 : 디버그 레이블은 고유할 필요는 없지만 일반적으로 구별 가능하게 만드는 것이 좋습니다.



onMount property


생성된 Atom 구성에는 onMount 선택적 속성이 있을 수 있습니다. onMount는 setAtom 함수를 취하고 선택적으로 onUnmount 함수를 반환하는 함수입니다.

onMount 함수는 Atom이 처음으로 공급자에 구독될 때 호출되고, onUnmount는 더 이상 구독되지 않을 때 호출됩니다. 어떤 경우에는(React strict 모드와 같은) Atom을 마운트 해제한 후 즉시 마운트할 수 있습니다.

const anAtom = atom(1)
anAtom.onMount = (setAtom) => {
  console.log('atom is mounted in provider')
  setAtom(c => c + 1) // 마운트 시 증가 카운트
  return () => { ... } // 선택적 onUnmount 함수를 반환합니다.
}

const Component = () => {
  // 다음과 같은 경우 구성요소가 마운트되면 'onMount'가 호출됩니다. 
  useAtom(anAtom)
  useAtomValue(anAtom)

  // 하지만, 다음과 같은 경우,
  // onMount 는 Atom 이 구독되지 않았기 때문에 호출되지 않습니다.
  useSetAtom(anAtom)
  useAtomCallback(
    useCallback((get) => get(anAtom), []),
  )
  // ...
}

setAtom 함수를 호출하면 Atom의 쓰기가 호출됩니다. 쓰기를 사용자 정의하면 동작을 변경할 수 있습니다.


const countAtom = atom(1)
const derivedAtom = atom(
  (get) => get(countAtom),
  (get, set, action) => {
    if (action.type === 'init') {
      set(countAtom, 10)
    } else if (action.type === 'inc') {
      set(countAtom, (c) => c + 1)
    }
  },
)
derivedAtom.onMount = (setAtom) => {
  setAtom({ type: 'init' })
}


참고 자료


https://jotai.org/docs/core/atom

profile
완전완전완전초보초보초보

0개의 댓글