Context API
이 전글의 예제에서도 그렇지만 리액트에서 여러개의 컴포넌트를 거쳐가면서 Props를 전달해줘야 하는 경우가 많다.
즉, A -> B -> C -> D 와같이 차근차근 Props를 넘겨주는 경우인데, 이를 A -> D로 한번에 넘길 수 있는 방법이 있다.
//ContextSample.js
import React, {createContext, useContext} from 'react';
function Child( {text} ) {
return <div>안녕하세요? {text} </div>
}
function Parent( { text }) {
return <Child text = {text} />
}
function GrandParent( { text }) {
return <Parent text = {text} />
}
function ContextSample() {
return <GrandParent text = "나는정민규" />
}
export default ContextSample;
다음과 같이 새 파일을 하나 만들고 보자. 현재 구조를 보면 Props가 차근차근 전달되는것을 알 수 있다. 이를 한번에 전달하는것으로 바꾸어 보자.
//ContextSample.js
import React, {createContext, useContext , useState} from 'react';
const MyContext = createContext('defaultValue');
function Child( ) {
const text = useContext(MyContext) //MyContext값을 가져오는 Hook.
return <div>안녕하세요? {text} </div>
}
function Parent() {
return <Child />
}
function GrandParent( { text }) {
return <Parent />
}
function ContextSample() {
const [value,setValue] = useState(true);
return (
<MyContext.Provider value = {value ? '나는정민규' : '나는저미규'}> { /* value값 설정 */ }
<GrandParent />
<button onClick= { () => setValue(!value)}>클릭</button>
</MyContext.Provider>
)
}
export default ContextSample;
다음과 같은 코드를 보면 Provider를통해 value값을 정해주고, 이렇게 저장된 MyContext의 값을 useContext를 통해서 불러오는 것을 볼 수 있다. 하나의 전역변수처럼 사용된다는 의미인데, 위 예제처럼 유동되는 값으로 설정할 수도 있다.
위 예제를 그대로 계속 사용했던 예제에 적용해 보자.
CreateUser 에게는 아무 props 도 전달하지 말것.
CreateUser 컴포넌트 내부에서 useInputs 를 사용할것.
useRef 를 사용한 nextId 값을 CreateUser 에서 관리할것.
위 3개의 조건을 충족시키도록 코드를 변형해 보았다.
App.js
//App.js
import React, {useRef,useReducer,useMemo,useCallback , createContext} from 'react';
import './App.css';
import CreateUser from './CreateUser';
import UserList from './UserList';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
const initialState = {
users : [
{
id : 1,
username : 'mingyu',
email : 'mingyu@naver.com',
active : true //항목 수정을 위해 추가
},
{
id : 2,
username : 'mingyu2',
email : 'mingyu2@naver.com',
active : false
},
{
id : 3,
username : 'mingyu3',
email : 'mingyu3@naver.com',
active : false
}
]
}
function reducer(state,action) {
switch (action.type){
case 'CREATE_USER':
return {
inputs : initialState.inputs, //inputs를 날리는 작업
users : state.users.concat(action.user) //더하는거
}
case 'TOGGLE_USER':
return{
...state,
users : state.users.map(user =>
user.id === action.id
? { ...user,active : !user.active}
: user
)
}
case 'REMOVE_USER':
return{
...state,
users : state.users.filter(user => user.id !== action.id)
}
default:
throw new Error('unhandled action');
}
}
export const UserDispatch = createContext(null); // dispatch를 내보내는 식으로 적용
function App() {
const [state,dispatch] = useReducer(reducer,initialState);
const { users } = state;
const count = useMemo(()=> countActiveUsers(users), [users])
return (
<UserDispatch.Provider value = {dispatch}>
< CreateUser />
<UserList users = {users} />
<div>활성 사용자 수 : {count}</div>
</UserDispatch.Provider>
);
}
export default App;
CreateUser.js
//CreateUser.js
import React, { useCallback, useContext,useRef } from 'react';
import { UserDispatch } from './App'; //App에서 Dispatch를 가져옴
import useInputs from './useInputs';
function CreateUser() {
const [{ username, email }, onChange, reset] = useInputs({
username: '',
email: ''
});
const dispatch = useContext(UserDispatch); // 가져온 Dispatch값을 불러옴
const nextId = useRef(4); // 3까지는 적어두었기 때문
const onCreate =() => {
dispatch({
type : 'CREATE_USER',
user : {
id : nextId.current,
username,
email,
}
});
nextId.current +=1
reset();
}
return (
<div>
<input
name='username'
placeholder='계정명'
onChange={onChange}
value={username}
/>
<input
name='email'
placeholder='email'
onChange={onChange}
value={email}
/>
<button onClick={onCreate}>등록</button>
</div>
)
}
export default React.memo(CreateUser);
즉, App.js에 있던 항목들을 안으로 옮기는 과정을 수행했다고 생각하면 될 것 같다.
immer
immer를 사용하면 불변성을 더 지키기 쉽다.
const object = {
a : 1,
b : 2
};
const nextobject = {
...object,
b : 3
};
위처럼 객체를 바꾸어줘야 불변성이 잘 지켜지게 된다.
이때 스프레드연산자(...) 를 넣어도 되지만 코드가 복잡해지는 경우 좀 어려워진다.
즉, immer를 사용하면 불변성을 해치는 코드를 작성하더라도 스스로 해준다.
먼저 터미널 창을열어서
yarn add immer
를통해 immer을 설치해준다.
이제 App.js 파일에
import produce from 'immer'
window.produce = produce;
다음과 같은 줄을 추가해서 개발자옵션 콘솔 칸에서 Produce의 역할을 간단히 살펴보겠다.
const state = {
number : 1,
dontChangeMe : 2
};
const nextState = produce (state,draft => {
draft.number +=1;
});
위와같이 상태를 바꾸어 준다면
nextState
// {number: 2, dontChangeMe: 2}
상태가 바뀐 것을 알 수 있다.
두번째 파라미터인 draft안에서 값을 바꾸면 알아서 불변성을 지켜주게 된다.
const array = [
{ id : 1, text : 'mingyu'},
{ id : 2, text : 'mingyu2'},
{ id : 3, text : 'mingyu3'}
]
//undefined
const nextarray = produce(array,draft =>{
draft.push({ id : 4, text : 'mingyu4' });
draft[0].text = draft[0].text + 'hi';
});
//undefined
nextarray
위 역시 안의 내용이 잘 바뀐 것을 확인할 수 있다.
자 그렇다면 App.js안의 아래 항목들을 리듀서 immer로 바꾸어 보자.
case 'CREATE_USER':
return {
inputs : initialState.inputs, //inputs를 날리는 작업
users : state.users.concat(action.user) //더하는거
}
case 'TOGGLE_USER':
return{
...state,
users : state.users.map(user =>
user.id === action.id
? { ...user,active : !user.active}
: user
)
}
case 'REMOVE_USER':
return{
...state,
users : state.users.filter(user => user.id !== action.id)
}
아래처럼 바뀔 수 있다.
function reducer(state,action) {
switch (action.type){
case 'CREATE_USER':
return produce(state,draft => { //immer 사용
draft.users.push(action.user);
})
case 'TOGGLE_USER':
return produce(state,draft => {
const user = draft.users.find(user => user.id === action.id);
user.active = !user.active;
})
case 'REMOVE_USER':
return produce(state,draft => {
const index = draft.users.findIndex(user => user.id === action.id);
draft.users.splice(index,1);
})
// return{
// ...state,
// users : state.users.filter(user => user.id !== action.id)
// }
default:
throw new Error('unhandled action');
}
}
로직이 그렇게 복잡하지 않은 경우에는 굳이 사용할 필요가 없다.
이전에 아래와 같이 함수형 업데이트를 사용하는 법에 공부한적이 있다.
const [todo,setTodo] = useState({
text : 'Hello',
done : false
});
const onClick = useCallback(() => {
setTodo(todo => ({
...todo,
done : !todo.done
}));
}, [] );
이경우 immer를 사용하면 보다 간편하게 적을 수 있다.
immer에는 아래와 같은 성질이 있다.
const [todo,setTodo] = useState({
text : 'Hello',
done : false
});
const updater = produce(draft => {
draft.done = !draft.done;
});
const nextTodo = updator(todo);
console.log(nextTodo);
// {text : 'Hello', done : true }
위처럼 produce에 첫번째 파라미터를 넣지 않는 경우 updater함수 자체를 반환하게 된다.
const [todo,setTodo] = useState({
text : 'Hello',
done : false
});
const onClick = useCallback(() => {
setTodo(
produce(draft => {
draft.done = !draft.done;
})
);
}, []);
즉 위처럼 produce를 바로 함수형 파라미터 에 넣어줄 수 있다.
데이터가 엄청 많지 않으면 immer와 일반 코드의 속도차이가 별로 없다는 것을 알고 있으면 될 것 같다. 그래도 속도지연이 있으니 필요한 곳에만 사용하는것이 좋다.
'FrontEnd > React' 카테고리의 다른 글
16_리액트_컴포넌트스타일링 (0) | 2021.12.24 |
---|---|
15_리액트_유용한 tool (0) | 2021.12.24 |
12_리액트_여러 Hook들 (0) | 2021.12.23 |
11_리액트_배열 랜더링 (0) | 2021.12.22 |
10_리액트_input상태,useRef (0) | 2021.12.22 |