처음 들어보는 얘기일겁니다. 방금 제가 만들었으니까요.
바로 react-eff-hook이라는 이름의 라이브러리입니다.
먼저 useEffect
에 대한 불만을 늘어놓는 것부터 시작해보죠.
count
를 1초마다 1씩 증가시키는 동작을 생각해봅시다.
useEffect(() => {
let cancelled = false
void (async () => {
while (!cancelled) {
await delay(1000)
setCount((x) => x + 1)
}
})()
return () => { cancelled = true }
}, [])
보통 위와 같은 방식으로 구현할텐데요. 별거아닌 동작인 것치고 너무 난리부르스를 떠는것 같지 않나요? 물론 그 난리에 다 이유는 있습니다.
(async () => { ... })()
useEffect
에 인자로 비동기 함수를 바로 쓸수 없어 이렇게 한번 감싸줘야합니다.return () => { cancelled = true; }
deps
의 값이 바뀔때 불리는 함수인데요. 이게 없다면 저 while 루프를 멈출 방법이 없습니다.cancelled = true
도 별로 마음에 안듭니다. 직관적이지 못하죠. 루프가 비동기 함수안에 있어서 이렇게 해야만 합니다. 실수로 빼먹기도 쉽습니다.이 문제들을 어떻게 해결하면 좋을까요?
import { useEff } from 'react-eff-hook'
useEff(function*() {
while (true) {
yield delay(1000)
setCount((x) => x + 1)
}
}, [])
react-eff-hook의 새로운 훅인 useEff
를 쓴 코드입니다.
function* 과 yield 구문이 생소한 분도 있을텐데요. Generator라고 해서 JavaScript의 코루틴 기능입니다. 이 단어들마저 생소하다면, 일단 그냥 써보는걸로 시작해도 좋습니다.
규칙은 간단합니다.
yield promise = await promise
useEff
내에서 좌변은 비동기 함수 내에서의 우변과 의미가 같습니다. 평소에 우변으로 쓰시던걸 좌변으로 바꿔쓰시기만 하면 됩니다.
반환 값의 타입이 필요한 경우에는
import { wait } from 'react-eff-hook'
// ...
const data = yield* wait(fetch())
로 쓰면 data
의 타입이 추론됩니다.
이제 어떤 점들이 개선되었는지 살펴봅시다.
일단 cancelled
가 없습니다. 루프는 알아서 멈춥니다. 무한 루프처럼 보이지만, 컴포넌트가 unmount할때 함께 죽습니다. 끝내주죠? 이게 코루틴입니다.
그래서 cleanup 함수도 없습니다. 애초에 cancelled
를 true
로 변경하는 용도였는데 더 이상 그럴 필요가 없으니까요.
어때요?
using 은 TypeScript 5.2에서 새로 도입된 문법입니다. 자세한 소개는 이쪽을 보시죠.
Python의 with 구문랑 비슷하다고 보시면 되고요. 그동안 TypeScript에선 이게 필요한 곳에 finally 구문을 툴툴 거리며 쓰고 있었습니다.
문제는, useEffect
와 이 친구가 서로 잘 안 맞는다는 겁니다. 한번 시도해보세요.
얼핏 생각하면 cleanup 함수를 Symbol.dispose
에 구현해놓으면 될것 같지만, 곧 안된다는걸 깨달을거에요. 호출되는 시점이 다르거든요.
하지만 우리에겐 useEff
가 있죠.
class Interval {
constructor(fn, interval) {
this.interval = setInterval(fn, interval);
}
[Symbol.dispose]() {
clearInterval(this.interval)
}
}
const forever = new Promise(() => {})
useEff(function*() {
using interval = new Interval(
() => { setCount((x) => x + 1) },
1000)
yield forever
}, [])
Interval
클래스는 선언된 스코프를 벗어날때 알아서 clearInterval
를 호출해 줍니다. 유령처럼 남아있는 타이머는 이제 안녕입니다.
forever
는 절대 resolve되지 않는 promise로써, yield forever
로 interval
이 해제되는것을 막아줍니다. 이 부분이 좀 요사스럽다고 생각할 수 있는데요. Interval
같은 걸 쓸때만 필요하고, 평소엔 거의 볼일이 없을겁니다.
잠시 생각해보면 저게 의미를 잘 나타낸다는 코드란 걸 금방 알수 있습니다.
한가지 주의할 점은, 에러를 throw 했을때의 동작이 useEffect
와 약간 다르단 겁니다. useEff
는 에러가 발생한 즉시 cleanup 함수를 호출합니다.
useEffect
는 그 안에서 호출된 비동기 함수의 에러는 무시하고, deps
가 변경되거나 컴포넌트가 unmount될때만 cleanup하죠.
저는 useEff
의 동작이 더 낫다고 생각하는데요. 예상하지 못한 시나리오가 있을 수 있어, 아직 100% 확신은 못합니다. 이 동작이 불편한 사례를 발견하면 알려주세요.
react-eff-hook은 작고 귀여운 라이브러리로, 기존 코드에 통합하는데 전혀 무리가 없습니다. 아직은 좀 미심쩍다면, 일부 useEffect
만 먼저 useEff
로 옮겨보세요.
슬슬 이게 더 낫다는 생각이 들기 시작하면, 별 하나만 부탁드릴게요.
저는 useEff
의 기능이 언젠가 React의 useEffect
에 포함되기를 바랍니다. breaking change없이 가능할 것입니다. 그렇게 될때까지 여러분들과 함께 연구해보고 싶습니다. 불편하거나 개선하고 싶은 부분이 있다면 언제든지 이슈트래커에 남겨주세요.
...였는데 검색해보니 이미 같은 내용의 RFC가 올라와 있더라고요. 2년이나 지났는데 진척이 없는걸로 보아, 실제로 도입될 가능성이 낮아보이는건 사실입니다.
using
키워드의 활용이 어필되기를 기대해봅니다.
감사합니다 사용해보겠습니다!