unstated-next
Unstated Next
- React Hooks 사용 가능
- 적은 용량
- React의 API 사용 가능
- 5분안에 배울 수 있는 최소한의 API
- TypeScript로 씌여졌고 React 코드를 더 쉽게 쓸 수 있도록 도와줌
그래서 리덕스랑 뭐가 달라? 🤷🏻
- 더 경량화 됨. 40배 더 작음
- 더 빠름. 성능 문제를 컴포넌트화 함
- 배우기 쉬움. React Hooks와 Context를 안다면 그냥 쓸 수 있음
- 통합하기 쉬움. 한 번에 하나의 컴포넌트를 통합하고, 모든 React 라이브러리와 쉽게 통합됨
- 테스트하기 쉬움.
- type 확인이 쉬움. 추론가능한 타입으로 만들 수 있게 해줌
- 알아야할 것이 최소한 임. 그냥 React
설치 방법 🛠
1 | npm install --save unstated-next |
Example 📖
1 | import React, { useState } from "react" |
API 📌
1. createContainer(useHook)
1 | import { createContainer } from "unstated-next" |
2. <Container.Provider>
1 | function ParentComponent() { |
3.<Container.Provider initialState>
1 | function useCustomHook(initialState = "") { |
4.Container.useContainer()
1 | function ChildComponent() { |
5. useContainer(Container)
1 | import { useContainer } from "unstated-next" |
Guide 🔍
만약 React Hooks를 써본 적이 없다면, React 공식문서 를 읽고 다시 돌아오는 것을 추천!
React Hooks만을 사용한 예
- hooks 를 사용해서 컴포넌트를 만들면 아래와 같은 모습이 됨
1 | function CounterDisplay() { |
- 컴포넌트 뒤의 로직을 공유하고 싶으면 커스텀 훅으로 빼낼 수 있음
1 | function useCounter() { |
- 하지만 로직 외에 상태를 공유하고 싶다면?
- context를 이용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContext(null)
function CounterDisplay() {
let counter = useContext(Counter)
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
function App() {
let counter = useCounter()
return (
<Counter.Provider value={counter}>
<CounterDisplay />
<CounterDisplay />
</Counter.Provider>
)
}
- context를 이용
이것만으로도 훌륭하고 완벽하며, 많은 사람들이 이런 식으로 코드를 작성해야 함.
하지만 우리는 일관되게 올바른 것을 얻기 위해 조금 더 구조적이고 의도가 분명한 API 디자인이 필요!
unstated-next 사용한 예
createContainer()
라는 함수를 사용함으로써, 우리는 커스텀 훅을 containers
라고 생각할 수 있고, 명확한 이름 덕에 잘못된 API 사용을 막을 수 있음
1 | import { createContainer } from "unstated-next" |
- 변경된 것들
- 만약 타입스크립트를 사용하고 있다면, 타입스크립트의 내장된 타입 추론 기능이 더 잘 작동하도록하는 이점이 있음.
- 커스텀 훅에 타입이 지정되어 있는 한, 모든 것이 작동 될 것임.
Tips 💁🏻
Tip #1: 컨테이너 작성
- 커스텀 훅으로 작업하기 때문에 다른 훅 내부에 컨테이너를 작성할 수 있음
1 | function useCounter() { |
Tip #2: 컨테이너 최소화하기
- 컨테이너를 작고 집중적으로 유지하는데 유용
- 컨테이너에서 로직을 코드 분할하려는 경우 중요
- 상태 값만 컨테이너에 남기고 다른 로직들은 자체 훅으로 이동
1 | function useCount() { |
Tip #3: 컴포넌트 최적화하기
- unstated-next 는 ‘최적화’할 필요가 없음
- 최적화하려면 표준 React 최적화를 해야 함
1 ) 컴포넌트를 분리해서 값 비싼 하위 트리 최적화
BEFORE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function CounterDisplay() {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
<div>
<div>
<div>
<div>SUPER EXPENSIVE RENDERING STUFF</div>
</div>
</div>
</div>
</div>
)
}AFTER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function ExpensiveComponent() {
return (
<div>
<div>
<div>
<div>SUPER EXPENSIVE RENDERING STUFF</div>
</div>
</div>
</div>
)
}
function CounterDisplay() {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
<ExpensiveComponent />
</div>
)
}
2 ) useMemo()로 값 비싼 작업 최적화
- useMemo() vs React.Memo
useMemo() |
React.Memo |
---|---|
React Hook | 고차 컴포넌트 (HOC) |
렌더링 사이에 반환된 값을 기억 | 렌더링 사이에 리액트 컴포넌트를 기억 |
의존성이 변경되었을 때만 메모이제이션된 값만 다시 계산 의존성이 없으면 렌더링 때마다 새 값 계산 |
props가 변경될때만 조건부로 업데이트 |
전달된 함수는 렌더링 중에 실행 | |
BEFORE
1
2
3
4
5
6
7
8
9
10
11
12
13
14function CounterDisplay(props) {
let counter = Counter.useContainer()
// Recalculating this every time `counter` changes is expensive
let expensiveValue = expensiveComputation(props.input)
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}AFTER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function CounterDisplay(props) {
let counter = Counter.useContainer()
// Only recalculate this value when its inputs have changed
let expensiveValue = useMemo(() => {
return expensiveComputation(props.input)
}, [props.input])
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
3 ) React.Memo()
와 useCallback()
을 사용해서 리렌더링 감소하기
BEFORE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContainer(useCounter)
function CounterDisplay(props) {
let counter = Counter.useContainer()
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}AFTER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function useCounter() {
let [count, setCount] = useState(0)
let decrement = useCallback(() => setCount(count - 1), [count])
let increment = useCallback(() => setCount(count + 1), [count])
return { count, decrement, increment }
}
let Counter = createContainer(useCounter)
let CounterDisplayInner = React.memo(props => {
return (
<div>
<button onClick={props.decrement}>-</button>
<p>You clicked {props.count} times</p>
<button onClick={props.increment}>+</button>
</div>
)
})
function CounterDisplay(props) {
let counter = Counter.useContainer()
return <CounterDisplayInner {...counter} />
}
4. useMemo()
로 요소 감싸기
BEFORE
1
2
3
4
5
6
7
8function CounterDisplay(props) {
let counter = Counter.useContainer()
let count = counter.count
return (
<p>You clicked {count} times</p>
)
}AFTER
1
2
3
4
5
6
7
8function CounterDisplay(props) {
let counter = Counter.useContainer()
let count = counter.count
return useMemo(() => (
<p>You clicked {count} times</p>
), [count])
}
Relation to Unstated (번역)
나는 이 라이브러리가 Unstated의 영적 후계자라고 생각한다. Unstated를 만든 이유는, React가 이미 상태 관리는 굉장히 잘 되어있다고 생각했고, 단지 상태와 로직을 쉽게 공유하는 것이 아쉽다고 생각했기 때문이다. 그래서 나는 React의 상태와 로직 공유를 위한 ‘최소한’의 해결책으로 Unstated를 만들었다.
하지만 Hooks로 React는 상태와 로직을 공유하기 더 좋아졌고, Unstated가 불필요한 추상화가 되었다고 생각이 들었다.
하지만 많은 개발자들이 ‘어플리케이션의 상태’를 위해 React Hooks로 상태와 로직을 공유하는 방법을 찾는 것에 어려움을 겪고 있다고 생각한다. 이는 문서화나 커뮤니티 추진력 문제일 수 있으나, API가 이러한 정신적 격차를 해소하는 데 도움이 될 수 있다고 생각한다.
앞서 말한 API란 Unstated Next이다. 이제는 “React에서 상태와 로직을 공유하기 위한 최소한의 API” 대신, “React에서 공유되는 상태와 로직을 이해하기 위한 최소한의 API”이다.
나는 항상 React의 편에 있고, React가 이기길 바란다. 커뮤니티가 Redux와 같은 상태 관리 라이브러리를 포기하고 React의 내장 툴체인을 사용하는 더 나은 방법을 찾고 싶다.
만약 당신이 Unstated 대신 React 자체만 사용하고 싶다면, 기꺼이 응원하고싶다. 블로그에 그것에 대한 이야기를 쓰고 얘기해줘라! 커뮤니티에 당신의 지식을 전파해라.