JotaiJotai

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

함수형 프로그래밍과 Jotai

예상치 못한 유사점

getter 함수를 충분히 오래 살펴보면, 특정 자바스크립트 언어 기능과 놀라울 정도로 비슷하다는 것을 알 수 있습니다.

const nameAtom = atom('Visitor')
const countAtom = atom(1)
const greetingAtom = atom((get) => {
const name = get(nameAtom)
const count = get(countAtom)
return (
<div>
Hello, {name}! You have visited this page {count} times.
</div>
)
})

이 코드를 asyncawait와 비교해 보세요:

const namePromise = Promise.resolve('Visitor')
const countPromise = Promise.resolve(1)
const greetingPromise = (async function () {
const name = await namePromise
const count = await countPromise
return (
<div>
Hello, {name}! You have visited this page {count} times.
</div>
)
})()

이 유사성은 우연이 아닙니다. 아톰과 Promise는 모두 모나드(Monads)†라는 함수형 프로그래밍 개념입니다. greetingAtomgreetingPromise에서 사용된 구문은 do-notation으로 알려져 있으며, 이는 더 단순한 모나드 인터페이스를 위한 문법적 설탕입니다.

모나드에 대해

모나드 인터페이스는 아톰과 Promise 인터페이스의 유연성을 담당합니다. 모나드 인터페이스 덕분에 greetingAtomnameAtomcountAtom으로 정의할 수 있었고, greetingPromisenamePromisecountPromise로 정의할 수 있었습니다.

궁금하다면, Atom이나 Promise와 같은 구조체가 모나드가 되려면 다음 함수들을 구현할 수 있어야 합니다. 재미있는 연습으로, 배열에 대해 of, map, join을 구현해 보는 것도 좋습니다.

type SomeMonad<T> = /* 예를 들어... */ Array<T>
declare function of<T>(plainValue: T): SomeMonad<T>
declare function map<T, V>(
anInstance: SomeMonad<T>,
transformContents: (contents: T) => V,
): SomeMonad<V>
declare function join<T>(nestedInstances: SomeMonad<SomeMonad<T>>): SomeMonad<T>

Promise와 아톰의 공통된 유산은 많은 패턴과 모범 사례가 둘 사이에서 재사용될 수 있음을 의미합니다. 그 중 하나를 살펴보겠습니다.

시퀀싱

콜백 지옥에 대해 이야기할 때, 우리는 보일러플레이트 코드, 들여쓰기, 그리고 놓치기 쉬운 실수에 대해 자주 언급합니다. 하지만 하나의 비동기 작업을 다른 하나의 비동기 작업에 연결하는 것이 콜백 문제의 끝은 아니었습니다. 만약 네트워크 호출을 네 번 하고 모두 기다려야 한다면 어떻게 될까요? 이런 코드 조각은 흔히 볼 수 있었습니다:

const nPending = 4
const results: string[]
function callback(err, data) {
if (err) throw err
results.push(data)
if (results.length === nPending) {
// results로 무언가를 한다...
}
}

하지만 결과가 서로 다른 타입을 가지고 있고 순서가 중요하다면 어떻게 될까요? 그렇다면 훨씬 더 많은 고통스러운 작업을 해야 합니다! 이 로직은 각 사용처에서 중복될 것이고, 실수하기 쉬울 것입니다. ES6부터는 단순히 Promise.all을 호출하면 됩니다:

declare function promiseAll<T>(promises: Array<Promise<T>>): Promise<Array<T>>

Promise.allArrayPromise를 "재배열"합니다. 이 개념, 즉 시퀀싱은 모든 모나드와 Traversable 쌍에 대해 구현될 수 있습니다. 많은 종류의 컬렉션이 Traversable에 속하며, 배열도 그 중 하나입니다. 예를 들어, 이는 아톰과 배열에 특화된 시퀀싱의 예입니다:

function sequenceAtomArray<T>(atoms: Array<Atom<T>>): Atom<Array<T>> {
return atom((get) => atoms.map(get))
}

정점

모나드는 수학자들에게는 60년 동안, 프로그래머들에게는 40년 동안 관심의 대상이었습니다. 모나드 패턴에 대한 많은 자료가 있습니다. 한번 살펴보세요! 여기 몇 가지 추천 자료를 소개합니다:

Promise를 사용하는 깔끔한 트릭을 배우는 것은 Promise.allsequenceAtomArray가 그랬던 것처럼 아톰에도 적용될 수 있습니다. 모나드는 마법이 아니며, 단지 유용한 도구일 뿐입니다. 알아두면 좋은 도구입니다.


참고

[†] ES6 Promise는 완전한 모나드가 아닙니다. 왜냐하면 다른 Promise를 중첩할 수 없기 때문입니다. 예를 들어, Promise<Promise<number>>는 의미상 Promise<number>와 동일합니다. 이 때문에 Promise는 .then만 가지고 있고, .map.flatMap을 모두 가지고 있지 않습니다. ES6 Promise는 "모나드적"이라고 설명하는 것이 더 적절할 것입니다.

ES6 Promise와 달리, ES6 Array는 완전히 합법적인 모나드입니다.