React 통합
사용 방법:
import { observer } from "mobx-react-lite" // 또는 "mobx-react".
const MyComponent = observer(props => ReactElement)
MobX는 React와 독립적으로 작동하지만, 일반적으로 React와 함께 사용합니다. MobX의 요점에서 이미 통합에 가장 중요한 부분을 확인했습니다. 바로 React 컴포넌트를 감쌀 수 있는 observer
HoC입니다.
observer
는 설치 중에 선택한 별도의 React 바인딩 패키지에서 제공됩니다. 아래 예시에서는 더 가벼운 mobx-react-lite
패키지를 사용할 것입니다.
import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react-lite"
class Timer {
secondsPassed = 0
constructor() {
makeAutoObservable(this)
}
increaseTimer() {
this.secondsPassed += 1
}
}
const myTimer = new Timer()
// `observer`로 감싸진 함수 컴포넌트는
// 이전에 사용했던 observable의 향후 변경 사항에 반응합니다.
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
setInterval(() => {
myTimer.increaseTimer()
}, 1000)
Hint: 위의 예제는 CodeSandbox에서 직접 실행해 볼 수 있습니다.
The observer
HoC는 렌더링 중에 사용되는 모든 observable에 React 컴포넌트들을 자동으로 구독합니다.
결과적으로 관련 observable이 변경되면 컴포넌트들을 자동으로 다시 렌더링합니다.
또한 관련된 변경사항이 없을 때는 컴포넌트가 다시 렌더링 되지 않습니다.
따라서, 컴포넌트로부터 접근할 수는 있지만 실제로 읽지 않는 observable은 다시 렌더링 되지 않습니다.
이러한 로직은 MobX 어플리케이션을 즉시 최적화 시키며 과도한 렌더링을 방지하기 위해 추가 코드를 작성할 필요가 없습니다.
observer
가 작동하려면 observable이 컴포넌트에 어떻게 도착하는지는 중요하지 않고 읽히기만 하면 됩니다.
observable을 깊게 읽는 것도 잘 작동하고, todos[0].author.displayName
처럼 복잡한 표현도 잘 작동합니다.
이러한 로직은 데이터 의존성을 명시적으로 선언하거나 미리 계산해야 하는 다른 프레임워크(selectors)에 비해 구독 메커니즘(mechanism)을 훨씬 더 정확하고 효율적으로 만듭니다.
로컬 및 외부 state
state를 구성하는 방법에는 큰 유연성이 있습니다. 왜냐하면 어떤 observable을 읽는지 또는 observable이 어디에서 유래했는지는 중요하지 않기 때문입니다.
아래 예제는 observer
로 감싸인 컴포넌트에서 외부 및 로컬 observable state를 사용하는 방법에 대해 다양한 패턴을 보여줍니다.
observer
컴포넌트에서 외부 state 사용하기
observable는 props로 컴포넌트에 전달할 수 있습니다. (아래 예시 처럼)
import { observer } from "mobx-react-lite"
const myTimer = new Timer() // 위의 타이머 정의를 참고하세요.
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)
// myTimer를 prop으로 전달합니다.
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
observable에 대한 참조를 얻는 방법은 중요하지 않으므로 외부 범위에서 직접 observable을 사용할 수 있습니다. (imports 포함)
const myTimer = new Timer() // 위의 타이머 정의를 참고하세요.
// `myTimer`는 props를 사용하지 않고 클로저로 인해 직접 사용됩니다.
const TimerView = observer(() => <span>Seconds passed: {myTimer.secondsPassed}</span>)
ReactDOM.render(<TimerView />, document.body)
observable을 직접 사용하는 것은 잘 작동하지만 일반적으로 모듈 state가 도입되기 때문에 이러한 패턴은 단위 테스트를 복잡하게 만들 수 있습니다. 그래서 전역 변수를 사용하는 대신 React Context를 사용하는 것이 좋습니다.
React Context는 전체 하위 트리와 observable을 공유하는 훌륭한 메커니즘입니다.
import {observer} from 'mobx-react-lite'
import {createContext, useContext} from "react"
const TimerContext = createContext<Timer>()
const TimerView = observer(() => {
// 컨텍스트에서 타이머를 가져옵니다.
const timer = useContext(TimerContext) // 위의 타이머 정의를 참고하세요.
return (
<span>Seconds passed: {timer.secondsPassed}</span>
)
})
ReactDOM.render(
<TimerContext.Provider value={new Timer()}>
<TimerView />
</TimerContext.Provider>,
document.body
)
Provider의 값을 다른 값으로 바꾸지 않는 것이 좋습니다. MobX를 사용하면 공유되는 observable이 자동으로 업데이트 되므로 Provider의 값을 다른 값으로 바꿀 필요가 없습니다.
observer
컴포넌트에서 로컬 observable state 사용하기
observer
가 사용하는 observable은 어디에서나 올 수 있으므로 로컬 state일 수도 있습니다.
다시 말해, 위에서 소개드린 옵션과는 다른 옵션도 사용할 수 있습니다.
로컬 observable state를 사용하는 가장 간단한 방법은 useState
를 사용하여 observable 클래스에 대한 참조를 저장하는 것입니다.
일반적으로 참조를 변경하는 경우는 드물기 때문에 useState
에서 반환된 업데이터 함수를 완전히 무시합니다.
import { observer } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const [timer] = useState(() => new Timer()) // 위의 타이머 정의를 참고하세요.
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
이전 예제에서 했던 것 처럼 타이머를 자동으로 업데이트하려면
useEffect
를 일반적인 React방식으로 사용할 수 있습니다.
useEffect(() => {
const handle = setInterval(() => {
timer.increaseTimer()
}, 1000)
return () => {
clearInterval(handle)
}
}, [timer])
앞에서 언급했듯이 클래스를 사용하는 대신 observable 객체를 직접 생성할 수 있습니다. 이를 위해 observable을 활용할 수 있습니다.
import { observer } from "mobx-react-lite"
import { observable } from "mobx"
import { useState } from "react"
const TimerView = observer(() => {
const [timer] = useState(() =>
observable({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
})
)
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
const [store] = useState(() => observable({ /* something */}))
은 일반적인 조합입니다.
이 패턴을 더 단순하게 만들기 위해 useLocalObservable
hook이 mobx-react-lite
패키지에 있으며, 이를 통해 이전 예제를 다음과 같이 단순화할 수 있습니다.
import { observer, useLocalObservable } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const timer = useLocalObservable(() => ({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
}))
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
observable state가 로컬로 필요하지 않을 수 있습니다.
이론적으로 React's Suspense 메커니즘의 일부 기능을 차단할 수 있으므로 로컬 컴포넌트 state에 대해 MobX observable을 너무 빨리 의존하지 않는 것이 좋습니다. 일반적으로 state가 컴포넌트(하위항목 포함)간 공유되는 도메인 데이터를 캡쳐할 때 MobX observable을 사용하세요. ex) todo items, users, bookings 등등
로딩 state, 선택 등과 같은 UI state만 캡쳐하는 state는 useState
hook을 사용하는 것이 더 좋습니다. 그렇게 하면 추후에 React suspense 기능을 사용할 수 있게 됩니다.
React 컴포넌트 안에서 observable을 사용하는 것은 깊거나(deep), computed 값이 있거나, 다른 observer
컴포넌트와 공유될 때 가치가 있습니다.
observer
컴포넌트 안에서 observable을 읽습니다.
항상 observer
를 언제 사용해야 할지 궁금하시나요? 일반적으로 observable 데이터를 읽는 모든 컴포넌트에 observer를 사용합니다.
observer
는 감싸고 있는 컴포넌트만 개선하며, 감싸고 있는 컴포넌트를 호출하는 컴포넌트는 개선하지 않습니다. 따라서 일반적으로 모든 컴포넌트는 observer에 의해 감싸져야 하며, 모든 컴포넌트를 observer로 감싸는 행동은 비효율적이지 않기 때문에 걱정하실 필요가 없습니다. observer
컴포넌트가 많을수록 업데이트의 세밀성이 더 높아져 렌더링 효율성이 높아집니다.
Tip: 가능한 한 늦게 객체에서 값을 가져옵니다.
observer
는 가능한 한 오랫동안 객체 참조를 전달할 때, 그리고 DOM과 low-level 컴포넌트로 렌더링 될 예정인 observer 기반 컴포넌트 내부의 속성만 읽을 때 가장 잘 동작합니다.
즉, observer
는 객체에서 값을 '역참조'한다는 사실에 반응합니다.
상단의 예시에서 TimerView
컴포넌트는 .secondsPassed
가 observer
컴포넌트 내부에서 읽는 것이 아니라 외부에서 읽혀 추적되지 않기 때문에 향후 변경사항에 반응하지 않습니다.
const TimerView = observer(({ secondsPassed }) => <span>Seconds passed: {secondsPassed}</span>)
React.render(<TimerView secondsPassed={myTimer.secondsPassed} />, document.body)
이러한 방법은 react-redux와는 다른 사고 방식입니다. react-redux에서는 메모이제이션을 더 잘 활용하기 위해 초기에 역참조하고 원시적인 값(primitives)을 전달하는 것이 더 좋은 관행입니다. 자세한 사항은 반응성 이해하기를 확인해주세요.
observer
가 아닌 컴포넌트에 observable을 전달하지 마세요.
observer
로 감싸진 컴포넌트는 컴포넌트 자체 렌더링 중에 사용되는 observable만 구독합니다.
따라서 observable objects·arrays·maps이 자식 컴포넌트에 전달되면 자식 컴포넌트들도 observer
로 감싸줘야 합니다.
위의 내용은 모든 콜백 요소 기반 컴포넌트들도 해당합니다.
observer
가 아닌 컴포넌트에 observable을 전달하려는 경우엔 전달하기 전에 observable을 일반 Javascript 값 또는 구조로 변환 해야합니다.
위의 내용을 자세히 설명하기 위해
observable todo
객체, TodoView
컴포넌트 (observer), 열(column)과 값(value) 매핑을 사용하지만, observer가 아닌 가상의 GridRow
컴포넌트를 예로 들어보겠습니다.
class Todo {
title = "test"
done = true
constructor() {
makeAutoObservable(this)
}
}
const TodoView = observer(({ todo }: { todo: Todo }) =>
// 잘못된 예시: GridRow는 observer가 아니기 때문에 todo.title 과 todo.done에 대한 변경 사항을 선택하지 않습니다.
return <GridRow data={todo} />
// 올바른 예시: `TodoView`가 `todo` 관련 변경사항을 감지하여
// 일반 데이터를 전달하도록 합니다.
return <GridRow data={{
title: todo.title,
done: todo.done
}} />
// 올바른 예시: `toJS`를 사용하는 것도 좋지만, 일반적으로는 명시적으로 사용하는 것이 더 좋습니다.
return <GridRow data={toJS(todo)} />
)
<Observer>
가 필요할 수 있습니다.
콜백 컴포넌트는 GridRow
가 onRender
콜백을 사용하는 동일한 예를 상상해보세요.
onRender
는 TodoView
의 렌더가 아니라 GridRow
의 렌더링 주기의 일부이기 때문에 (구문상 표시가 되는 위치에 있음에도 불구하고) 콜백 컴포넌트가 observer 컴포넌트를 사용하는지 확인해야 합니다.
또는 <Observer />
을 사용하여 인라인 익명 observer를 만들 수 있습니다.
const TodoView = observer(({ todo }: { todo: Todo }) => {
// 잘못된 예시: GridRow.onRender는 observer가 아니기 때문에 todo.title / todo.done의 변경사항을 선택하지 않습니다.
return <GridRow onRender={() => <td>{todo.title}</td>} />
// 올바른 예시: 변경사항을 감지 할 수 있도록 Observer에 콜백 렌더링을 감쌉니다.
return <GridRow onRender={() => <Observer>{() => <td>{todo.title}</td>}</Observer>} />
})
Tips
Note: mobx-react vs. mobx-react-lite
해당 문서에서는 기본적으로 mobx-react-lite
를 사용했습니다.
mobx-react는 더 거대하며 mobx-react-lite
를 안에서 사용합니다.
mobx-react는 그린필드 프로젝트(greenfield project)에서는 필요하지 않은 몇 가지 기능을 더 제공합니다.
mobx-react가 제공하는 추가 기능은 다음과 같습니다.
- React 클래스 컴포넌트를 지원합니다.
Provider
그리고inject
를 제공합니다. React.createContext가 더 이상 필요하지 않습니다.- 명확한 observable
propTypes
.
mobx-react
는 함수형 컴포넌트를 지원하며 mobx-react-lite
를 완전히 다시 패키징하고 내보냅니다.
mobx-react
를 사용하면 mobx-react-lite
를 의존성으로 추가하거나 다른곳에서 가져올 필요가 없습니다.
import React from "React"
const TimerView = observer(
class TimerView extends React.Component {
render() {
const { timer } = this.props
return <span>Seconds passed: {timer.secondsPassed} </span>
}
}
)
자세한 내용은 mobx-react docs을 확인해주세요.
Tip: React DevTools에서 멋진 컴포넌트 이름 사용하기
React DevTools는 컴포넌트 계층 구조를 적절하게 표시하기 위해 컴포넌트의 display name을 사용합니다.
아래와 같이 사용하는 경우
export const MyComponent = observer(props => <div>hi</div>)
DevTools에 no display name이 보일 것입니다.
위의 문제를 해결하기 위해 다음과 같이 접근할 수 있습니다.
화살표 함수 대신에 이름이 있는
function
을 사용하세요.mobx-react
는 함수 이름에서 컴포넌트 이름을 유추합니다.export const MyComponent = observer(function MyComponent(props) { return <div>hi</div> })
TypeScript와 Babel같은 변환기는 변수 이름에서 컴포넌트 이름을 유추합니다.
const _MyComponent = props => <div>hi</div> export const MyComponent = observer(_MyComponent)
default export를 사용하여 변수 이름에서 추론합니다.
const MyComponent = props => <div>hi</div> export default observer(MyComponent)
[Broken]
displayName
을 명시적으로 설정합니다.export const MyComponent = observer(props => <div>hi</div>) MyComponent.displayName = "MyComponent"
이것은 React 16에서 작동되지 않습니다. mobx-react
observer
는 React.memo를 사용하고 다음과 같은 버그(https://github.com/facebook/react/issues/18026)를 발생 시키지만, React 17에서 수정될 것입니다.
이제 컴포넌트 이름을 볼 수 있습니다.
observer
가 다른 데코레이터나 고차 컴포넌트와 결합하는 경우, observer
가 가장 안쪽에 있는 (처음 적용된) 데코레이터 인지 확인하세요.
그렇지 않으면 정상적으로 작동하지 않습니다.
import { observer, useLocalObservable } from "mobx-react-lite"
import { useEffect } from "react"
const TimerView = observer(({ offset = 0 }) => {
const timer = useLocalObservable(() => ({
offset, // The initial offset value
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
},
get offsetTime() {
return this.secondsPassed - this.offset // 'props'로 넘어온 'offset'이 아닙니다!
}
}))
useEffect(() => {
// `props`로 넘어온 offset을 observable `timer`와 동기화합니다.
timer.offset = offset
}, [offset])
// 데모를 위해 Effect를 활용하여 타이머를 설정 합니다.
useEffect(() => {
const handle = setInterval(timer.increaseTimer, 1000)
return () => {
clearInterval(handle)
}
}, [])
return <span>Seconds passed: {timer.offsetTime}</span>
})
ReactDOM.render(<TimerView />, document.body)
실제로 이러한 패턴은 거의 필요하지 않으며,
return <span>Seconds passed: {timer.secondsPassed - offset}</span>
이 약간 덜 효율적이지만 훨씬 간단합니다.
useEffect
는 리액트 컴포넌트의 라이프 사이클에서 발생해야하는 부수효과(side effects)를 설정하는데 사용할 수 있습니다.
useEffect
를 사용하려면 의존성을 지정해야합니다.
MobX는 이미 effect의 의존성을 자동으로 결정하는 방법인 autorun
을 가지고 있기 때문에 useEffect
에 의존성을 지정하는 작업이 필요하지 않습니다.
useEffect
를 사용하여 autorun
과 컴포넌트의 수명주기를 합치는 것은 다행히 간단합니다.
import { observer, useLocalObservable, useAsObservableSource } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const timer = useLocalObservable(() => ({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
}))
// observable의 변경사항에 따라 트리거 되는 Effect입니다.
useEffect(
() =>
autorun(() => {
if (timer.secondsPassed > 60) alert("Still there. It's a minute already?!!")
}),
[]
)
// 데모를 위해 Effect를 활용하여 타이머를 설정 합니다.
useEffect(() => {
const handle = setInterval(timer.increaseTimer, 1000)
return () => {
clearInterval(handle)
}
}, [])
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
effect함수에서 autorun
에 의해 생성된 disposer를 반환한다는 점에 유의하세요.
컴포넌트가 사라질 때 autorun
이 정리되기 때문에 중요합니다!
observable로 지정되지 않은 값이 autorun을 다시 실행해야 하는 경우를 제외하면 의존성 배열을 비워 둘 수 있고, 다시 실행 해야하는 경우에는 의존성 배열을 추가해야합니다. linter를 만족스럽게 만들기 위해 타이머(위의 예시처럼)를 의존성으로 정의할 수 있습니다. 참조가 실제로 변경되지 않기 때문에 안전하고 더 이상 영향을 미치지 않습니다.
어떤 observable이 효과(effect)를 트리거 해야하는지 명시적으로 정의하려면 다른 패턴들은 유지하고 autorun
대신 reaction
을 사용하세요.
React 컴포넌트를 어떻게 최적화할 수 있나요?
React 최적화 {🚀}를 확인해주세요.
문제해결 방법
도와주세요. 컴포넌트가 리렌더링 되지 않아요...
observer
를 잊지 않았는지 확인하세요.- 반응하려는 대상이 실제로 observable인지 확인해보세요. 런타임에 확인할 경우
isObservable
,isObservableProp
와 같은 유틸리티를 사용해보세요. - 브라우저의 콘솔 로그에 경고 또는 에러가 있는지 확인해보세요.
- 일반적으로 추적이 어떻게 작동하는지 확인해보세요. 반응성 이해하기를 참고하세요.
- 위에서 설명하고 있는 잘못된 예시를 확인해보세요.
- 잘못된 메커니즘 사용에 대해 경고하고 콘솔 로그를 확인할 수 있도록 MobX를 설정하세요.
- trace를 사용하여 올바른 구독을 하고 있는지 확인하거나 spy, mobx-logger 패키지를 사용하여 MobX가 일반적으로 무엇을 하는지 확인해보세요.