const value = useContext(MyContext);
Prop Drilling
은 React에서 props를 이용해 컴포넌트 간 데이터를 전달할 때 해당 데이터가 필요없는 컴포넌트도 거지는 것이다.
위 그림처럼 컴포넌트 A의 데이터를 컴포넌트 C로 전달하기 위해 사이에 있는 컴포넌트 B를 거쳐야한다.
문제점
prop 전달이 깊어질수록 해당 prop를 추적하기 힘들어지고 유지보수가 어려워진다.
context는 React 컴포넌트 트리 안에서 global이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법으로 context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다.
React Hook인 useContext를 이용하면 이러한 Context를 더 편하게 사용할 수 있다.
context API
createContext
: context 객체 생성Provider
: 생성한 context를 하위 컴포넌트에 전달Consumer
: context의 변화를 감시하는 컴포넌트Ex)
App.js
import React, { createContext } from "react";
import Children from "./Children";
// AppContext 객체를 생성한다.
export const AppContext = createContext();
const App = () => {
const user = {
name: "김채원",
job: "가수"
};
return (
<>
<AppContext.Provider value={user}>
<div>
<Children />
</div>
</AppContext.Provider>
</>
);
};
export default App;
Children.js
import React from "react";
import { AppContext } from "./App";
const Children = () => {
return (
<AppContext.Consumer>
{(user) => (
<>
<h3>AppContext에 존재하는 값의 name은 {user.name}입니다.</h3>
<h3>AppContext에 존재하는 값의 job은 {user.job}입니다.</h3>
</>
)}
</AppContext.Consumer>
);
};
export default Children;
여러 컴포넌트가 context를 사용하게 되면 코드가 점점 복잡해지는 문제가 있다.
useContext를 적용하면 코드가 아래와 같이 바뀐다.
import React, { useContext } from "react";
import { AppContext } from "./App";
const Children = () => {
// useContext를 이용해서 따로 불러온다.
const user = useContext(AppContext);
return (
<>
<h3>AppContext에 존재하는 값의 name은 {user.name}입니다.</h3>
<h3>AppContext에 존재하는 값의 job은 {user.job}입니다.</h3>
</>
);
};
export default Children;
App.js에서 Context를 생성하고 Provider를 통해 전달하는 코드는 그대로지만 Children.js에서 AppContext
를 사용하는 과정에서 const user = useContext(AppContext)
를 이용해 Context를 불러온 후 바로 사용이 가능하게 바뀐다.
const [<상태 객체>, <dispatch 함수>] = useReducer(<reducer 함수>, <초기 상태>, <초기 함수>);
reducer 함수
dispatch 함수
행동(action) 객체
→ 즉, 컴포넌트에서 dispatch 함수에 행동(action) 객체를 던지면, reducer 함수가 이 행동(action)에 따라서 상태(state)를 변경해준다.
useReducer()
Hook 함수를 이용해 현재 카운트 값과 2개 버튼을 보여주는 간단한 카운터 컴포넌트
import React, { useReducer } from "react";
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<h2>{state.count}</h2>
<button onClick={() => dispatch({ type: "INCREMENT", step: 1 })}>
</button>
<button onClick={() => dispatch({ type: "DECREMENT", step: 1 })}>
</button>
</>
);
}
INCREMENT
또는 DECREMENT
가 넘어가고, step 속성에는 변경할 값의 크기를 넘긴다.useReducer()
함수는 첫번째 인자로 넘어오는 reducer 함수를 통해 컴포넌트의 state가 action에 따라 어떻게 변해야하는지를 정의한다.
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + action.step };
case "DECREMENT":
return { count: state.count - action.step };
default:
throw new Error("Unsupported action type:", action.type);
}
}
INCREMENT
타입의 행동에 대해서는 현재 카운트 값을 step 만큼 증가하여 새로운 상태를 반환하고, DECREMENT
타입의 행동에 대해서는 현재 카운트 값을 step 만큼 감소하여 새로운 상태를 반환한다. 위 예제 정도의 간단한 상태 관리를 위해서라면 간단하게 useState()
함수를 써도 된다.
더 복잡한 상태 관리를 위해 카운트의 하한 값과 상한 값을 제한하고, 카운트의 값을 무작위로 바꾸는 버튼과 초기화 시키는 버튼을 추가했다.
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return state.count < action.max
? { count: state.count + action.step }
: state;
case "DECREMENT":
return state.count > action.min
? { count: state.count - action.step }
: state;
case "RESET":
return initialState;
case "RANDOM":
return {
count:
Math.floor(Math.random() * (action.max - action.min)) + action.min,
};
default:
throw new Error("Unsupported action type:", action.type);
}
}
import React, { useReducer } from "react";
function Counter({ step = 1, min = 0, max = 10 }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>
단계: {step}, 최소: {min}, 최대: {max}
</p>
<h2>{state.count}</h2>
<button onClick={() => dispatch({ type: "INCREMENT", step, max })}>
증가
</button>
<button onClick={() => dispatch({ type: "DECREMENT", step, min })}>
감소
</button>
<button onClick={() => dispatch({ type: "RANDOM", min, max })}>
무작위
</button>
<button onClick={() => dispatch({ type: "RESET" })}>초기화</button>
</>
);
}
상태 로직이 복잡해져도 카운터 컴포넌트 코드는 크게 복잡해지지 않는다.
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback()
은 함수를 menoization 하기 위해서 사용되는 Hook 함수이다.
첫번째 인자로 넘어온 함수를, 두번째 인자로 넘어온 배열 내의 값이 변경될 때 저장해놓고 재사용할 수 있게 해준다.
어떤 React 컴포넌트 안에 함수가 선언이 되어 있다면, 이 함수는 해당 컴포넌트가 렌더링될 때 마다 새로운 함수가 생성된다.
하지만, useCallback()
을 사용하면, 해당 컴포넌트가 렌더링되더라도 그 함수가 의존하는 값이 바뀌지 않는 한 기존 함수를 계속해서 반환한다.
const add = useCallback(() => x + y, [x, y]);
x
또는 y
값이 바뀌면 새로운 함수가 생성되어 add 변수에 할당x
와 y
값이 동일하면 다음 랜더링 때 이 함수 재사용자바스크립트가 브라우저에서 빠르게 실행되므로 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해 useCallback()
을 사용하는 것은 큰 의미가 없거나 오히려 손해일 수 있다.
useCallback()
함수가 어떻게 쓰일 때 의미있는 성능 향상을 보일까?
> const add1 = () => x + y;
undefined
> const add2 = () => x + y;
undefined
> add1 === add2
false
자바스크립트에서 함수도 객체로 취급하기 때문에 메모리 주소에 의한 참조 비교가 일어나 동일한 코드의 함수를 ===
연산자를 통해 비교하면 false
가 반환된다.
이런 자바스크립트 특성은 React 컴포넌트 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제로 이어질 수 있다.
많은 React Hook 함수들이 불필요한 작업을 줄이기 위해 두번째 인자로 첫번째 함수가 의존하는 배열을 받는다.
import React, { useState, useEffect } from "react";
function Profile({ userId }) {
const [user, setUser] = useState(null);
const fetchUser = () =>
fetch(`https://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({ user }) => user);
useEffect(() => {
fetchUser().then((user) => setUser(user));
}, [fetchUser]);
// ...
}
예를 들어, 다음과 같은 컴포넌트에서 API를 호출하는 코드는 fetchUser
이 변경될 때만 호출된다. 위에서 말한 자바스크립트가 함수의 동등성을 판단하는 방식 대문에 예상치 못한 무한 루프가 발생한다.
fetchUser
은 함수이기 때문에, userId
값이 바뀌는 말든 컴포넌트가 랜더링될 때 마다 새로운 참조값으로 변경된다. 그러면 useEffect()
함수가 호출되어 user
상태값이 바뀌고 그러면 다시 컴포넌트가 랜더링되고 그럼 또 다시 useEffect()
함수가 호출되는 것이 반복된다.
import React, { useState, useEffect } from "react";
function Profile({ userId }) {
const [user, setUser] = useState(null);
const fetchUser = useCallback(
() =>
fetch(`https://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({ user }) => user),
[userId]
);
useEffect(() => {
fetchUser().then((user) => setUser(user));
}, [fetchUser]);
// ...
}
useCallback()
을 사용하면 컴포넌트가 다시 랜더링 되더라도 fetchUser
함수의 참조값을 동일하게 유지시킬 수 있다.
따라서 의도대로 useEffect()
에 넘어온 함수는 userId
값이 변경되지 않는 한 재호출 되지 않게 된다.
memoization
이란 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말한다.
memoization
을 적절하게 사용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화 할 수 있다.
함수형 컴포넌트은 React 앱에서 랜더링이 일어날 때마다 호출이 된다. React에서 컴포넌트의 랜더링은 한 번만 일어나는 것이 아니라 수시로 계속 일어날 수 있다.
함수형 컴포넌트 내에 내부적으로 매우 복잡한 연산을 수행하는 함수가 들어있으면 컴포넌트의 재랜더링이 필요할 때마다 이 함수가 호출이 되므로 UI에 지속적으로 지연이 발생할 것이다.
function MyComponent({ x, y }) {
const z = compute(x, y);
return <div>{z}</div>;
}
랜더링이 일어날 때 마다, compute
함수의 인자로 넘어오는 x
와 y
값이 항상 바뀌는 게 아니라면 굳이 compute
함수를 계속 호출할 필요가 없다.
→ 랜더링이 발생했을 때 이전 랜더링과 현재 랜더링 간에 x
와 y
값이 동일한 경우, 다시 함수를 호출하여 z
값을 구하는 대신, 기존에 메모리에 저장해두었던 z
값을 그대로 사용한다.
이런 경우에 memoization을 간편하게 사용할 수 있게 하는 것이 useMemo()
이다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
첫번째 인자 : 결과값을 생성하는 팩토리 함수
두번째 인자 : 기존 결과값 재활용 여부의 기준이 되는 입력값 배열
function MyComponent({ x, y }) {
const z = useMemo(() => compute(x, y), [x, y]);
return <div>{z}</div>;
}
x
와 y
값이 이 전에 랜더링했을 때와 동일할 경우, 이 전 랜더링 때 저장해두었던 결과값을 재활용한다.x
와 y
값이 이 전에 랜더링했을 때와 달라졌을 경우, () => compute(x, y) 함수를 호출하여 결과값을 새롭게 구해 z
에 할당한다.const refContainer = useRef(initialValue);
프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환
useRef는 매번 렌더링을 할 때 동일한 ref 객체를 제공 → 리렌더링 되지 않는다.
useRef는 저장공간 또는 DOM 요소에 접근하기 위해 사용되는 React Hook이다.
자바스크립트를 사용할 때, 특정 DOM을 선택하기 위해서 querySelector 등의 함수를 썼다면, React를 사용하는 프로젝트에선 useRef라는 리액트 훅을 사용한다.
무한 로프 예시
function App() {
const [count, setCount] = useState(1);
const [renderingCount, setRedneringCount] = useState(1);
useEffect(() => {
console.log("rendering Count : ", renderingCount);
setRedneringCount(renderingCount + 1);
});
return (
<div>
<div>Count : {count}</div>
<button onClick={() => setCount(count + 1)}> count up </button>
</div>
);
}
useEffect 안에 있는 setRedneringCount()가 계속해서 컴포넌트를 리랜더링해서 무한 루프에 빠지게 된다.
useRef 사용
function App() {
const [count, setCount] = useState(1);
const renderingCount = useRef(1);
useEffect(() => {
console.log("renderingCount : ", renderingCount.current);
++renderingCount.current;
});
return (
<div>
<div>Count : {count}</div>
<button onClick={() => setCount(count + 1)}> count up </button>
</div>
);
}
useRef로 관리하는 값은 값이 변해도 화면이 랜더링 되지 않기 때문에 무한 루프가 발생하지 않는다.
import { useRef, useEffect } from "react";
import "./styles.css";
function App() {
const inputRef = useRef();
function focus() {
inputRef.current.focus();
console.log(inputRef.current);
}
return (
<div>
<input ref={inputRef} type="text" placeholder="아이디 또는 이메일" />
<button>Login</button>
<br />
<button onClick={focus}>focus</button>
</div>
);
}
export default App;
useRef를 통해 input
DOM에 접근해 focus할 수 있음