Context API
✨ 전역 상태 관리
전역적으로 관리되고 있는 상태 값에 접근하여 사용할 수 있는 것
🤔 전역 상태 관리는 언제 필요할까?
로그인한 유저 정보 | Theme style |
이 밖에도 웹 어플리케이션을 개발하다보면 전역 상태로 관리해야 하는 값들이 꽤 많이 생긴다.
이제 전역 상태 관리는 잠시 잊고 props에 대해 생각해보자.
App이라는 최상위 컴포넌트가 있고 그 아래로 A, B, C, D 컴포넌트(children)가 있다.
D가 그려야 하는 정보를 A 컴포넌트가 가지고 있다고 가정해보자.
props는 어떤 데이터를 하위 요소에 전달할 수 있는 좋은 방법 중 하나이다.
그러나 props의 한계는 바로 위 요소인 C 요소에게 props를 전달 받을 수 밖에 없다.
A 컴포넌트가 가지고 있는 요소를 D가 사용하려면 A ➡ B ➡ C ➡ D 로 props를 전달받아야 한다.
❌ B, C 컴포넌트들은 중간에 필요하지 않은 로직 처리를 해야하기 때문에 좋은 구조가 아니다.
이렇게 props를 계속 전달하는 것을 props drilling이라고 한다.
🤔 어떻게 하면 props drilling을 피할 수 있을까?
❗ React의 Context API를 사용하면 된다!
A 컴포넌트에 어떤 Context를 생성하면 하위 요소들이 Context에 직접 접근할 수 있는 권한을 가지게 된다.
좀 더 복잡한 상황을 가정해보자.
App 에서 Context를 생성할 경우, 하위 요소들 전부 App의 Context에 직접 접근할 수 있는 권한이 생기게 된다.
이런 경우는 어떨까?
A 컴포넌트가 Context를 생성하게 되면 A 컴포넌트의 하위 요소들만 A 요소의 Context에 접근할 수 있고, 나머지 요소들은 접근할 수 없다.
Context를 사용하는 예시
App.js (메인)
import { BrowserRouter, Routes, Route } from "react-router-dom";
import BlogPage from "./components/BlogPage";
import UserStore from "./store/user";
function App() {
return (
<>
<UserStore>
<BrowserRouter>
<Routes>
<Route path={"blog"} element={<BlogPage />} />
</Routes>
</BrowserRouter>
</UserStore>
</>
);
}
export default App;
user.js (Context를 생성하는 상위 요소)
import React, {createContext, useState} from 'react';
export const UserContext = createContext();
export default function UserStore(props) {
const [job, setJob] = useState("FE-developer");
const user = {
name: "plum",
job,
changeJob: (updateJob) => {
setJob(updateJob)
},
};
return (
<UserContext.Provider value={user}>
{props.children}
</UserContext.Provider>
);
}
BlogPage.js (Context에 접근하는 하위 요소)
import React, { useContext } from "react";
import { UserContext } from "../store/user";
export default function BlogPage() {
const value = useContext(UserContext);
console.log(value);
return (
<div>
<h1>BlogPage</h1>
</div>
);
}
React.createContext
const MyContext = React.createContext(defaultValue);
Context 객체를 만듭니다.
Context 객체를 구독하고 있는 컴포넌트를 렌더링할 때 React는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider로부터 현재값을 읽습니다.
defaultValue 매개변수는 트리 안에서 적절한 Provider를 찾지 못했을 때만 쓰이는 값입니다.
이 기본값은 컴포넌트를 독립적으로 테스트할 때 유용한 값입니다.
Provider를 통해 undefined을 값으로 보낸다고 해도 구독 컴포넌트들이 defaultValue 를 읽지는 않는다는 점에 유의하세요.
Context.Provider
<MyContext.Provider value={/* 어떤 값 */}>
Context 오브젝트에 포함된 React 컴포넌트인 Provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 합니다.
Provider 컴포넌트는 value prop을 받아서 이 값을 하위에 있는 컴포넌트에게 전달합니다. 값을 전달받을 수 있는 컴포넌트의 수에 제한은 없습니다. Provider 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값(가장 가까운 Provider의 값)이 우선시됩니다.
Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 됩니다.
Provider로부터 하위 consumer(.contextType와 useContext을 포함한)로의 전파는 shouldComponentUpdate 메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트됩니다.
context 값의 바뀌었는지 여부는 Object.is와 동일한 알고리즘을 사용해 이전 값과 새로운 값을 비교해 측정됩니다.
useReducer
https://ko.reactjs.org/docs/hooks-reference.html
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer의 세번째 인자에는 어떠한 함수가 들어가는데, 해당 인자는 잘 사용하지 않으므로 앞선 두 가지의 인자에 대해 잘 알아두자.
✨ 첫번째 인자
: useState의 대체함수.
(state, action) => newState의 형태로 reducer를 받고 dispatch 메서드와 짝의 형태로 현재 state를 반환합니다.
예시 코드를 살펴보자.
아래는 useState 내용에 있던 카운터 예시인데 reducer를 사용해서 다시 작성한 것이다.
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
어떤 버튼을 클릭하면 dispatch 메서드를 실행한다. 이 때, action 객체를 받는다.
dispatch 메서드를 실행하게 되면 reducer가 실행된다.
🤔 어떨 때 useState를 사용하고 어떨 때 useReducer를 사용할까?
useState를 사용할 때, setState를 하기 전에 state를 조작하는 로직이 길어질 상황이 생긴다.
그렇게 되면 컴포넌트 코드의 길이가 길어지고 복잡해진다.
이러한 상황을 막기 위해 useReducer를 사용하여 state를 조작하는 로직을 컴포넌트 밖으로 뺄 수 있다. (ex. reducer 함수)
또한, 다음 state가 이전 state에 의존적인 경우에 보통 useState보다 useReducer를 선호한다.
setState를 반복적으로 호출했을 때, 비동기로 한 번에 (병합되어) 처리된다. 그렇기 때문에 여러 번 setState를 호출해도 각각 반영이 되지 않을 수 있다.
각각 반영이 됐으면 좋겠어 😭 ➡ useState보단 useReducer를 선호 ❗
또한 useReducer는 자세한 업데이트를 트리거 하는 컴포넌트의 성능을 최적화할 수 있게 하는데,
이것은 콜백 대신 dispatch를 전달 할 수 있기 때문입니다.
콜백 전달을 피하는 법?
Todo 앱을 만든다고 가정을 해보자.
const TodosDispatch = React.createContext(null);
function TodosApp() {
// 주의: `dispatch`는 다시 렌더링 간에 변경되지 않습니다
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
TodosApp 내의 트리에 있는 모든 자식은 dispatch 기능을 사용하여 TodosApp에 작업을 전달할 수 있다.
function DeepChild(props) {
// 작업을 수행하려면 context에서 dispatch를 얻을 수 있습니다.
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
<button onClick={handleClick}>Add todo</button>
);
}
이런 식으로 useReducer는 Context API와 함께 사용이 많이 된다.
useReducer와 Context API가 함께 사용하는 예시를 보자.
App.js (메인)
import { BrowserRouter, Routes, Route } from "react-router-dom";
import BlogPage from "./components/BlogPage";
import UserStore from "./store/user";
function App() {
return (
<>
<UserStore>
<BrowserRouter>
<Route path={"blog"} element={<BlogPage />} />
</Routes>
</BrowserRouter>
</UserStore>
</>
);
}
export default App;
user.js (Context를 생성하고 dispatch를 내려주는 상위 요소)
import React, { createContext, useState, useReducer } from "react";
export const UserContext = createContext();
const initialUser = {
name: "plum",
job: "FE-developer",
};
const userReducer = (state, action) => {
switch (action.type) {
case "changeJob":
// state의 job에 해당하는 데이터를 action.text
return { ...state, job: action.text };
default:
break;
}
};
export default function UserStore(props) {
const [user, dispatch] = useReducer(userReducer, initialUser);
console.log(user)
return (
<UserContext.Provider value={dispatch}>{props.children}</UserContext.Provider>
);
}
BlogPage.js (Context를 내려받고 dispatch를 통해 값을 변경하는 하위 요소)
import React, { useContext } from "react";
import { UserContext } from "../store/user";
export default function BlogPage() {
const dispatch = useContext(UserContext);
console.log(dispatch);
return (
<div>
<h1>BlogPage</h1>
<button
onClick={() => dispatch({ type: "changeJob", text: "BE-engineer" })}
>
Change Job
</button>
</div>
);
}
Redux
리액트 어플리케이션을 만들 때 전역 상태 관리를 도와주는 라이브러리이다.
앞서 context API + useState or useReducer를 사용하면 전역 상태 관리를 할 수 있다고 배웠다.
이 세 가지는 리액트에 기본적으로 내장되어 있는 함수이기 때문에 별도의 라이브러리 설치 필요 없이 전역 상태 관리를 할 수 있다.
🤔 라이브러리 없이 전역 상태 관리를 할 수 있는데, 사람들은 왜 Redux를 사용할까?
2022년 기준, Redux가 trendy한 기술은 아니다.
그러나 2016~2019년도에 많이 사용이 되었기 때문에 현업에 가서도 자주 사용할 것이다.
🤔 그렇다면, 과거에 사람들은 왜 Redux를 많이 사용했을까?
context API + useState or useReducer를 사용하는 것보다 성능적인 면에서 좀 더 좋다.
Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 됩니다.
➡ 불필요한 렌더링이 발생하게 된다.
https://ko.redux.js.org/introduction/getting-started/
Redux 시작하기
Redux는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너입니다.
Redux는 여러분이 일관적으로 동작하고, 서로 다른 환경(서버, 클라이언트, 네이티브)에서 작동하고, 테스트하기 쉬운 앱을 작성하도록 도와줍니다.
React Redux 앱 만들기
React와 Redux로 새 앱을 만들기 위해 추천하는 방법은 Create React App를 위한 공식 Redux+JS 템플릿을 사용하는 것입니다. 이를 통해 Redux Toolkit와 React Redux가 React 컴포넌트와 통합되는 이점을 누릴 수 있습니다.
npx create-react-app my-app --template redux
Redux 코어
Redux 코어 라이브러리는 NPM에서 패키지로 받아 모듈 번들러나 Node 앱에서 사용 가능합니다.
이미 CRA를 생성한 뒤라면 아래와 같이 생성한다.
# NPM
npm install redux
# Yarn
yarn add redux
기본 예제
여러분의 앱의 상태 전부는 하나의 저장소(store)안에 있는 객체 트리에 저장됩니다. 상태 트리를 변경하는 유일한 방법은 무엇이 일어날지 서술하는 객체인 액션(action)을 보내는 것 뿐입니다. 액션이 상태 트리를 어떻게 변경할지 명시하기 위해 여러분은 리듀서(reducers)를 작성해야 합니다.
import { createStore } from 'redux'
/**
* 이것이 (state, action) => state 형태의 순수 함수인 리듀서입니다.
* 리듀서는 액션이 어떻게 상태를 다음 상태로 변경하는지 서술합니다.
*
* 상태의 모양은 당신 마음대로입니다: 기본형(primitive)일수도, 배열일수도, 객체일수도,
* 심지어 Immutable.js 자료구조일수도 있습니다. 오직 중요한 점은 상태 객체를 변경해서는 안되며,
* 상태가 바뀐다면 새로운 객체를 반환해야 한다는 것입니다.
*
* 이 예제에서 우리는 `switch` 구문과 문자열을 썼지만,
* 여러분의 프로젝트에 맞게
* (함수 맵 같은) 다른 컨벤션을 따르셔도 좋습니다.
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// 앱의 상태를 보관하는 Redux 저장소를 만듭니다.
// API로는 { subscribe, dispatch, getState }가 있습니다.
let store = createStore(counter)
// subscribe()를 이용해 상태 변화에 따라 UI가 변경되게 할 수 있습니다.
// 보통은 subscribe()를 직접 사용하기보다는 뷰 바인딩 라이브러리(예를 들어 React Redux)를 사용합니다.
// 하지만 현재 상태를 localStorage에 영속적으로 저장할 때도 편리합니다.
store.subscribe(() => console.log(store.getState())))
// 내부 상태를 변경하는 유일한 방법은 액션을 보내는 것뿐입니다.
// 액션은 직렬화할수도, 로깅할수도, 저장할수도 있으며 나중에 재실행할수도 있습니다.
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1
useReducer와 상당히 유사한 것을 알 수 있다.
Redux 작동 원리
Redux 사용 예시 중 Todo 를 만든 코드가 있다.
다음 명령어로 Todos 예제를 실행할 수 있다.
git clone https://github.com/reduxjs/redux.git
cd redux/examples/todos
npm install
npm start
아니면 sandbox에서 확인하자.
Redux를 사용하여 직접 Todo 앱을 만들어보자!
Redux Toolkit
Redux Toolkit은 Redux 로직을 작성하기 위해 저희가 공식적으로 추천하는 방법입니다. RTK는 Redux 앱을 만들기에 필수적으로 여기는 패키지와 함수들을 포함합니다. 대부분의 Redux 작업을 단순화하고, 흔한 실수를 방지하며, Redux 앱을 만들기 쉽게 해주는 모범 사례를 통해 만들어졌습니다.
RTK는 저장소 준비, 리듀서 생산과 불변 수정 로직 작성, 상태 "조각" 전부를 한번에 작성 등 일반적인 작업들을 단순화해주는 유틸리티를 포함하고 있습니다.
1. 저장소 준비
configureStore() 함수를 대표적으로 사용한다.
basic example
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({ reducer: rootReducer })
// The store now has redux-thunk added and the Redux DevTools Extension is turned on
full example
// file: todos/todosReducer.ts noEmit
import type { Reducer } from '@reduxjs/toolkit'
declare const reducer: Reducer<{}>
export default reducer
// file: visibility/visibilityReducer.ts noEmit
import type { Reducer } from '@reduxjs/toolkit'
declare const reducer: Reducer<{}>
export default reducer
// file: store.ts
import { configureStore } from '@reduxjs/toolkit'
// We'll use redux-logger just as an example of adding another middleware
import logger from 'redux-logger'
// And use redux-batched-subscribe as an example of adding enhancers
import { batchedSubscribe } from 'redux-batched-subscribe'
import todosReducer from './todos/todosReducer'
import visibilityReducer from './visibility/visibilityReducer'
const reducer = {
todos: todosReducer,
visibility: visibilityReducer,
}
const preloadedState = {
todos: [
{
text: 'Eat food',
completed: true,
},
{
text: 'Exercise',
completed: false,
},
],
visibilityFilter: 'SHOW_COMPLETED',
}
const debounceNotify = _.debounce(notify => notify());
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
devTools: process.env.NODE_ENV !== 'production',
preloadedState,
enhancers: [batchedSubscribe(debounceNotify)],
})
// The store has been created with these options:
// - The slice reducers were automatically passed to combineReducers()
// - redux-thunk and redux-logger were added as middleware
// - The Redux DevTools Extension is disabled for production
// - The middleware, batched subscribe, and devtools enhancers were composed together
full example에서 configureStore에 reducer 외에 새로운 데이터들이 추가 되었다.
여기서 제일 중요한 데이터는 middleware이다.
2. 리듀서 생산과 불변 수정 로직 작성
reducer 작성과 불변 로직을 좀 더 쉽게 해주는 createReducer() 함수가 있다.
먼저 우리가 createReducer() 함수를 사용하지 않고 reducer 함수를 작성한 예시를 살펴보자.
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'increment':
return { ...state, value: state.value + 1 }
case 'decrement':
return { ...state, value: state.value - 1 }
case 'incrementByAmount':
return { ...state, value: state.value + action.payload }
default:
return state
}
}
action에 대해 switch문을 돌아 조건에 따라 state 값을 변경하였다.
createReducer() 함수를 사용하면 switch문을 사용하지 않고 바로 state 값을 변경할 수 있다.
import { createAction, createReducer } from '@reduxjs/toolkit'
const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
const incrementByAmount = createAction('counter/incrementByAmount')
const initialState = { value: 0 }
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state, action) => {
state.value++
})
.addCase(decrement, (state, action) => {
state.value--
})
.addCase(incrementByAmount, (state, action) => {
state.value += action.payload
})
})
3. 상태 "조각" 전부를 한번에 작성
대표적으로 createSlice() 함수를 사용한다.
import { createSlice } from '@reduxjs/toolkit'
const initialState = { value: 0 }
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value++
},
decrement(state) {
state.value--
},
incrementByAmount(state, action) {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
Action과 reducer를 한 번에 작성할 수 있다.
또한 extraReducer를 추가할 수 있다.
비동기 처리와 상태 관리, middleware
지금부터 API 호출과 전역 상태 관리를 한 번에 묶어서 같이 생각해보자.
가장 흔한 상황인 로그인한 유저 정보를 전역 상태로 관리하는 상황이다.
이런 로직을 어떻게 처리하는 것이 좋을까?
앞 부분은 비동기로 처리를 하고 뒷 부분은 전역 스토어를 업데이트한다.
1. 컴포넌트의 useEffect내에서 API 호출하고, 응답 받은 결과를 스토어에 업데이트 한다.
dispatch({ tpye: "updateUser", payload : {nickname: 김코딩, purchased: 01 패키지}})
2. 컴포넌트의 useEffect 내에서 dispatch({type:"updateUser"})로 액션 객체만을 보내고,
user Store의 reducer 안에서 API를 호출하고, 응답 받은 결과를 스토어에 업데이트 한다.
결론
2번 방식은 불가능 하다! ❌
🤔 why? reducer는 순수함수이기 때문이다.
순수 함수 : 동일한 인자가 주어졌을 때 항상 동일한 결과를 반환하는 함수
아래는 불가능한 2번 예제 코드이다.
그렇다면 1번째 방식은 가능할까 불가능할까?
가능하다!! 🙆♂️
하지만, 1번 방식을 사용하게 된다면 컴포넌트에서 해야 할 일이 너무 많다...
비지니스 로직이 붙어있는 컴포넌트 코드는 좋은 코드가 아니다.
그렇다면 우리는 어떻게 해야할까? 😢
✨ middleware를 사용하자!
middleware
action 객체를 dispatch 하고,
reducer가 해당 action 객체에 대해 Store를 업데이트 하기 전에 추가적인 작업을 할 수 있게 도와준다.
ex. 비동기 처리, 로깅...
Redux를 사용한다면 middleware를 사용할 수 있기 때문에 선호도가 높다.
Redux-thunk
비동기 통신을 위한 redux-middleware
redux-thunk | redux-saga | redux-observable |
dispatch에 action 객체가 아닌 thunk 함수를 전달한다. 가장 간단해서 진입 장벽이 낮다. |
generator를 활용한다. redux-thunk가 가지고 있는 몇 가지 아쉬움을 보완한다. 현업에서 사용하는 프로젝트에서 많이 사용해왔다. |
RxJS를 기반으로 한다. 가장 진입 장벽이 높다. |
redux tool kit에 createAsyncThunk() 함수를 활용하면
다른 미들웨어를 사용하지 않아도 비동기 통신 후에 전역 상태를 업데이트 할 수 있다.
redux tool kit이 활성화 되지 않았을 때 위에 언급했던 middleware가 자주 사용되었다.
middleware와 createAsyncThunk() 함수 간 어떤 차이점이 있는 지 알아보면 좋을 것 같다.
https://redux-toolkit.js.org/api/createAsyncThunk
상태관리 도구들 소개 (Recoil, Jotai, constate)
|
||
- 리액트 팀에서 직접 만든 상태관리 라이브러리 - 비동기 데이터 통신을 위한 기능 제공 - React 내부에 접근이 가능하여 동시성 모드, Suspense 등을 손쉽게 지원 가능 |
- Recoil에서 영향을 받아 일본에서 만들어진 라이브러리 | - React Context + State - Context의 단점을 개선 |
Recoil
https://recoiljs.org/ko/docs/introduction/motivation
주요 개념
Recoil을 사용하면 atoms (공유 상태)에서 selectors (순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있다. Atoms는 컴포넌트가 구독할 수 있는 상태의 단위다. Selectors는 atoms 상태값을 동기 또는 비동기 방식을 통해 변환한다.
Atoms
Atoms는 atom함수를 사용해 생성한다.
const fontSizeState = atom({
key: 'fontSizeState',
default: 14,
});
Atoms는 디버깅, 지속성 및 모든 atoms의 map을 볼 수 있는 특정 고급 API에 사용되는 고유한 키가 필요하다. 두개의 atom이 같은 키를 갖는 것은 오류이기 때문에 키값은 전역적으로 고유하도록 해야한다. React 컴포넌트의 상태처럼 기본값도 가진다.
컴포넌트에서 atom을 읽고 쓰려면 useRecoilState라는 훅을 사용한다. React의 useState와 비슷하지만 상태가 컴포넌트 간에 공유될 수 있다는 차이가 있다.
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return (
<button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
Click to Enlarge
</button>
);
}
버튼을 클릭하면 버튼의 글꼴 크기가 1만큼 증가하며, fontSizeState atom을 사용하는 다른 컴포넌트의 글꼴 크기도 같이 변화한다.
function Text() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return <p style={{fontSize}}>This text will increase in size too.</p>;
}
Selector
Selector는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수(pure function)다. 상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다. 컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며 selectors가 변경되면 컴포넌트들도 다시 렌더링된다.
Selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다. 최소한의 상태 집합만 atoms에 저장하고 다른 모든 파생되는 데이터는 selectors에 명시한 함수를 통해 효율적으로 계산함으로써 쓸모없는 상태의 보존을 방지한다.
Selectors는 어떤 컴포넌트가 자신을 필요로하는지, 또 자신은 어떤 상태에 의존하는지를 추적하기 때문에 이러한 함수적인 접근방식을 매우 효율적으로 만든다.
컴포넌트의 관점에서 보면 selectors와 atoms는 동일한 인터페이스를 가지므로 서로 대체할 수 있다.
Selectors는 selector함수를 사용해 정의한다.
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
});
get 속성은 계산될 함수다. 전달되는 get 인자를 통해 atoms와 다른 selectors에 접근할 수 있다. 다른 atoms나 selectors에 접근하면 자동으로 종속 관계가 생성되므로, 참조했던 다른 atoms나 selectors가 업데이트되면 이 함수도 다시 실행된다.
이 fontSizeLabelState 예시에서 selector는 fontSizeState라는 하나의 atom에 의존성을 갖는다. 개념적으로 fontSizeLabelState selector는 fontSizeState를 입력으로 사용하고 형식화된 글꼴 크기 레이블을 출력으로 반환하는 순수 함수처럼 동작한다.
Selectors는 useRecoilValue()를 사용해 읽을 수 있다. useRecoilValue()는 하나의 atom이나 selector를 인자로 받아 대응하는 값을 반환한다. fontSizeLabelState selector는 writable하지 않기 때문에 useRecoilState()를 이용하지 않는다. (writable한 selectors에 대한 더 많은 정보는 selector API reference에 자세히 기술되어 있다.)
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
const fontSizeLabel = useRecoilValue(fontSizeLabelState);
return (
<>
<div>Current font size: ${fontSizeLabel}</div>
<button onClick={setFontSize(fontSize + 1)} style={{fontSize}}>
Click to Enlarge
</button>
</>
);
}
버튼를 클릭하면 버튼의 글꼴 크기가 증가하는 동시에 현재 글꼴 크기를 반영하도록 글꼴 크기 레이블을 업데이트하는 두 가지 작업이 수행된다.
Recoil 에서는 비동기 통신까지 다룬다. (심화)
https://recoiljs.org/ko/docs/guides/asynchronous-data-queries
Recoil은 데이터 플로우 그래프를 통해 상태를 매핑하는 방법과 파생된 상태를 리액트 컴포넌트에 제공합니다. 가장 강력한 점은 graph에 속한 함수들도 비동기가 될 수 있다는 것입니다.
Synchronous Example (동기 예제)
여기 user 이름을 얻기위한 간단한 동기 atom 과 selector를 예로 들어보겠습니다.
const currentUserIDState = atom({
key: 'CurrentUserID',
default: 1,
});
const currentUserNameState = selector({
key: 'CurrentUserName',
get: ({get}) => {
return tableOfUsers[get(currentUserIDState)].name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameState);
return <div>{userName}</div>;
}
function MyApp() {
return (
<RecoilRoot>
<CurrentUserInfo />
</RecoilRoot>
);
}
Asynchronous Example (비동기 예제)
만약 user의 이름이 쿼리 해야하는 데이터베이스에 저장되어 있었다면, Promise를 리턴하거나 혹은 async 함수를 사용하기만 하면 됩니다. 의존성에 하나라도 변경점이 생긴다면, selector는 새로운 쿼리를 재평가하고 다시 실행시킬겁니다. 그리고 결과는 쿼리가 유니크한 인풋이 있을 때에만 실행되도록 캐시됩니다.
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
return response.name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
Selector의 인터페이스는 동일하므로 컴포넌트에서는 selector를 사용하면서 동기 atom 상태나 파생된 selector 상태, 혹은 비동기 쿼리를 지원하는지 신경쓰지 않아도 괜찮습니다!
하지만, React 렌더 함수가 동기인데 promise가 resolve 되기 전에 무엇을 렌더 할 수 있을까요? Recoil은 보류중인 데이터를 다루기 위해 React Suspense와 함께 동작하도록 디자인되어 있습니다. 컴포넌트를 Suspense의 경계로 감싸는 것으로 아직 보류중인 하위 항목들을 잡아내고 대체하기 위한 UI를 렌더합니다.
function MyApp() {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</RecoilRoot>
);
}
Error 또한 react <ErrorBoundary>로 잡을 수 있습니다.
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
if (response.error) {
throw response.error;
}
return response.name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
'React' 카테고리의 다른 글
[React] Why React (0) | 2022.07.28 |
---|---|
[Medium] 341. Flatten Nested List Iterator (0) | 2022.07.27 |
[React] 비동기 프로그래밍과 API 호출 (0) | 2022.07.26 |
[React] 리스트와 key / 폼과 이벤트 제어하기 (0) | 2022.07.26 |
[React] Lifecycle와 useEffect() (0) | 2022.07.25 |
Comment