#3. Context API와 기타 Hooks
Context API
React.createContext(defaultValue) => {Provider, Consumer}
React.createContext 함수를 호출하면 Context 객체가 생성됩니다. 생성된 context 객체로부터 상위 컴포넌트에서는 Provider 컴포넌트를 이용해서 데이터를 전달하고 하위 컴포넌트에서는 Consumer 컴포넌트를 이용해서 데이터를 사용하는 구조입니다.
const UserContext = React.createContext(default);
아래 코드는 Provider 컴포넌트로부터 전달된 데이터를 두개의 하위 컴포넌트에서 사용하는 코드입니다. useContext 훅을 사용하여 Consumer 컴포넌트를 생략한 컴포넌트와 그렇지 않은 컴포넌트가 있습니다.
중간에 OuterComponent의 경우에는 데이터의 전달과정과 무관하므로 React.memo를 사용하였습니다. 또한 props의 경우와 마찬가지로 상탯값 변경 함수를 Context로 전달하여 하위 컴포넌트에서 상위 컴포넌트의 상탯값을 수정할 수도 있습니다.
import React, { useState, useContext } from "react";
const CountContext = React.createContext(0);
const SetCountContext = React.createContext(() => { });
export default function MyComponent() {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={count}>
<SetCountContext.Provider value={setCount}>
<OuterComponent />
</SetCountContext.Provider>
</CountContext.Provider>
)
}
const OuterComponent = React.memo(() => {
return (
<div>
<h1>안녕하세요 OuterComponent입니다.</h1>
<InnerWithUseContext />
<InnerWithoutUseContext />
</div>
)
})
function InnerWithUseContext() {
const count = useContext(CountContext);
const setCount = useContext(SetCountContext);
return (
<div>
<h3>{`현재 카운트: ${count}`}</h3>
<button onClick={() => setCount(prev => prev + 1)}>증가</button>
</div>
)
}
function InnerWithoutUseContext() {
return (
<CountContext.Consumer>
{count => (
<SetCountContext.Consumer>
{setCount => (
<React.Fragment>
<h3>{`현재 카운트: ${count}`}</h3>
<button onClick={() => setCount(prev => prev + 1)}>증가</button>
</React.Fragment>
)}
</SetCountContext.Consumer>
)}
</CountContext.Consumer>
)
}
메모이제이션 훅: useMemo, useCallback
useMemo와 useCallback은 이전 값을 기억해서 성능을 최적화하는 용도로 사용됩니다. 먼저 useMemo부터 살펴보겠습니다.
useMemo
function MyComponent({v1, v2}) {
const value = useMemo(() => runHeavyJob(v1, v2), [v1, v2]);
return <p>{`value is ${value}`}</p>
}
useMemo 훅은 계산량이 많은 함수의 반환값을 재활용하는 용도로 사용됩니다. 첫번째 매개변수로 함수를 입력하고 두번째 매개변수로 의존성 배열을 입력합니다. 의존성 배열 안에 들어있는 값이 변경되지 않으면 useMemo 훅은 함수의 이전 반환값을 재사용합니다.
useCallback
function Profile() {
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const onSave = useCallback(() => saveToServer(name, age), [name, age]);
return (
<div>
<p>{`name is ${name}`}</p>
<p>{`age is ${age}`}</p>
<UserEdit onSave={onSave} setName={setName} setAge={setAge} />
</div>
)
}
useCallback 훅은 리액트의 렌더링 성능을 위해 제공되는 훅입니다. 컴포넌트가 렌더링될 때마다 자식 컴포넌트에 함수로 넘겨준 속성값은 계속해서 새로 생성됩니다. 따라서 속성값이 매번 변경되기 때문에 React.memo를 사용해도 불필요한 렌더링이 발생한다는 문제점이 있습니다. 이러한 문제점을 해결하기 위해서 useCallback 훅이 등장하게 되었습니다.
useMemo 훅과 마찬가지로 의존성 배열 안의 값이 변경되지 않으면 이전에 생성된 함수를 재사용합니다. 즉 name과 age 값이 변경되지 않으면 UserEdit 컴포넌트의 onSave 속성값으로 항상 같은 함수가 전달되기 때문에 불필요한 렌더링이 발생하지 않게 됩니다.
상탯값을 리덕스처럼 관리하기: useReducer
useReducer 훅을 사용하면 컴포넌트의 상탯값을 리덕스의 리듀서처럼 관리할 수 있습니다.
import React, { useReducer } from "react";
const INITIAL_STATE = { name: "empty", age: 0 };
function reducer(state, action) {
switch (action.type) {
case "setName":
return { ...state, name: action.name };
case "setAge":
return { ...state, age: action.age };
default:
return state;
}
}
export default function Profile() {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
return (
<div>
<p>{`name is ${state.name}`}</p>
<p>{`age is ${state.age}`}</p>
<input
type="text"
value={state.name}
onChange={e =>
dispatch({ type: "setName", name: e.currentTarget.value })
}
/>
<input
type="number"
value={state.age}
onChange={e =>
dispatch({ type: "setAge", age: e.currentTarget.value })
}
/>
</div>
)
}
useReducer 훅을 통해 redux처럼 데이터를 전달할 수 있습니다.
트리 깊은 곳으로 이벤트 함수 전달
보통 상위 컴포넌트에서 다수의 상탯값을 관리합니다. 이때 자식 컴포넌트로부터 발생한 이벤트에서 상위 컴포넌트의 상탯값을 변경해야 하는 경우가 많습니다. 이를 위해서는 상위 컴포넌트에서 트리의 깊은 곳까지 이벤트 처리 함수를 전달해야 합니다.
useReducer 훅과 Context API를 이용하면 다음과 같이 손쉽게 상위 컴포넌트에서 트리 깊은 곳으로 이벤트 처리 함수를 전달할 수 있습니다.
import React, { useReducer, useContext } from "react";
const ProfileDispatchContext = React.createContext(null);
const INITIAL_STATE = { name: "Mike", age: 25 };
function reducer(state, action) {
switch (action.type) {
case "setName":
return { ...state, name: action.name };
case "setAge":
return { ...state, age: action.age };
default:
return state;
}
}
export default function Profile() {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
return (
<div>
<p>{state.name}</p>
<p>{state.age}</p>
<ProfileDispatchContext.Provider value={dispatch}>
<InnerComponent name={state.name} />
</ProfileDispatchContext.Provider>
</div>
)
}
function InnerComponent(props) {
const dispatch = useContext(ProfileDispatchContext);
return (
<div>
<input
type="text"
value={props.name}
onChange={e =>
dispatch({ type: "setName", name: e.currentTarget.value })
}
/>
</div>
)
}
Context 객체의 Provider를 통해 dispatch 함수를 하위 모든 컴포넌트에서 사용할 수 있도록 전달하였습니다. 하위 컴포넌트에서는 useContext 훅을 통해 dispatch를 받아온 뒤 dispatch 함수를 통해 상위 컴포넌트의 상탯값을 변경하고 있습니다.
부모 컴포넌트에서 접근 가능한 함수 구현: useImperativeHandle
클래스형 컴포넌트에서는 ref 객체를 통해 자식 컴포넌트의 메서드를 호출할 수 있었습니다. 이번엔 반대로 useImperativeHandle 훅을 이용하여 상위 컴포넌트에서 하위 컴포넌트의 함수에 접근할 수 있도록 해보겠습니다. useImperativeHandle 훅을 이용하면 마치 함수형 컴포넌트에도 메서드가 있는 것처럼 만들 수 있습니다.
외부로 공개할 함수 정의하기
import React, { forwardRef, useState, useImperativeHandle, useRef } from "react";
const Profile = forwardRef((props, ref) => {
const [name, setName] = useState("");
const [age, setAge] = useState(0);
useImperativeHandle(ref, () => ({
addAge: value => setAge(age + value),
getNameLength: () => name.length,
}))
return (
<div>
<p>{`name is ${name}`}</p>
<p>{`age is ${age}`}</p>
</div>
)
})
// 부모 컴포넌트
export default function Parent() {
const profileRef = useRef();
const onClick = () => {
if (profileRef.current) {
profileRef.current.addAge(5);
console.log(`name Length: ${profileRef.current.getNameLength()}`);
}
}
return (
<div>
<Profile ref={profileRef} />
<button onClick={onClick}>add age 5</button>
</div>
)
}
부모 컴포넌트에서 useRef 훅을 통해 반환된 ref 객체를 자식 컴포넌트의 ref 속성값으로 전달하면 부모 컴포넌트에서 ref객체의 current 속성을 통해 자식 컴포넌트에서 useImperativeHandle에서 미리 정의해둔 메서드들을 사용할 수 있습니다.