최근 React를 사용하면서 hooks api를 적극적으로 사용하고 있습니다. 더 나은 코드와 최적화를 위해 useReducer
, useCallback
, useMemo
를 사용하곤 있지만, 아직 헷갈리는 개념이 있어서 제대로 알아보자는 차원에서 정리해보겠습니다.
React 공식 문서에선 useReducer
가 useState
를 대체할 수 있다고 설명하고 있습니다. 우선 useState
만으로 아이디와 비밀번호를 입력받는 로그인 화면 컴포넌트를 구현해보겠습니다.
function Signin() {
const [id, setId] = React.useState('')
const [password, setPassword] = React.useState('')
return (
<form>
<h1>로그인</h1>
<input name="id" type="text" placeholder="아이디 입력"
value={id} onChange={event => setId(event.target.value)} />
<input name="password" type="password" placeholder="비밀번호 입력"
value={password} onChange={event => setPassword(event.target.value)} />
</form>
)
}
useState
를 두번 사용해서 구현했습니다.
이를 useReducer
를 사용하면 다음과 같이 구현할 수 있습니다.
const reducer = (state, newState) => ({ ...state, ...newState })
function Signin() {
const [inputValue, setInputValue] = React.useReducer(reducer, { id: '', password: '' })
return (
<form>
<h1>로그인</h1>
<input name="id" type="text" placeholder="아이디 입력"
value={inputValue.id}
onChange={event => setInputValue({ [event.target.name]: event.target.value })} />
<input name="password" type="password" placeholder="비밀번호 입력"
value={inputValue.password}
onChange={event => setInputValue({ [event.target.name]: event.target.value })} />
</form>
)
}
사용법은 기본 Javascript의 reduce
와 유사하니 사용법을 알고있다면, 이해하기 쉽다고 생각합니다. reduce mdn 문서 링크.
React 공식 사이트의 useReducer로 카운터를 구현한 예제를 보면, action
, dispatch
등의 개념을 도입해서 Redux의 reducer와 매우 유사한 구조로 구현할 수도 있습니다.
React 공식 useReducer
예제
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>
</>
);
}
useReducer
를 어떻게 활용해야할까요? useState
를 많이 사용하는 경우에 사용하면 코드가 깔끔해지겠지만, 사실 다음 예시처럼 한번만 사용해도 구현 가능합니다.
function Signin() {
const [inputValue, setInputValue] = React.useState({ id: '', password: '' })
return (
<form>
<h1>로그인</h1>
<input name="id" type="text" placeholder="아이디 입력"
value={inputValue.id}
onChange={event => setInputValue({ ...inputValue, [event.target.name]: event.target.value })} />
<input name="password" type="password" placeholder="비밀번호 입력"
value={inputValue.password}
onChange={event => setInputValue({ ...inputValue, [event.target.name]: event.target.value })} />
</form>
)
}
물론 전개 구문 사용한 ...inputValue
을 중복해서 사용하는 것은 코드가 깔끔해보이지는 않습니다. 하지만 이것은 이벤트 핸들러 함수를 분리한다던지, 커스텀 훅을 만든다던지 등으로 어떻게 구현하냐에 따라 다르게 느껴질거 같습니다. 또한 나중에는 context API와 연동해서 redux를 완전히 대체하는데 쓰일 수 있지 않을까란 생각이 듭니다.
이는 최적화를 위한 훅입니다. React에서 이벤트를 핸들링 할 때 보통 다음 코드처럼 컴포넌트 내부에 함수를 선언해서 사용합니다.
function Component() {
const handleClick = () => console.log('clicked!')
return (
<button onClick={handleClick}>클릭해보세요!</button>
)
}
위 코드는 별 문제가 되지 않습니다. 하지만 컴포넌트가 렌더링 될 때 마다 함수를 새로 생성한다는 단점이 있습니다. 부모 컴포넌트가 렌더링되거나, 상태(state)가 변경되는 경우, React 컴포넌트는 렌더링을 유발합니다. 다음 코드를 살펴봅시다.
function Component() {
const [count, setCount] = React.useState(0)
const handleClick = () => console.log('clicked!')
return (
<>
<button onClick={() => setCount(count + 1)}>카운트 올리기</button>
<button onClick={handleClick}>클릭해보세요!</button>
</>
)
}
버튼을 클릭해서 count
값을 변경하면 컴포넌트 내부의 상태를 변경하고, 재렌더링을 유발합니다. 함수 컴포넌트의 렌더링이란, 다음 코드처럼 컴포넌트 함수가 새로 호출됨을 의미하고 이는 렌더링 때마다 새로 handleClick
함수를 생성합니다.
Component() // count는 0. 최초 렌더링
// setCount(count + 1)
Component() // count는 1. 두번째 렌더링
// setCount(count + 1)
Component() // count는 2. 세번째 렌더링
// setCount(count + 1)
Component() // count는 3. 네번째 렌더링
이러면 불필요한 메모리를 낭비하고 최적화도 좋지 않습니다. 특정 상태의 변경과 상관없는 함수의 경 useCallback
을 사용하면 매번 새로 생성되는 것을 방지할 수 있습니다.
function Component() {
const [count, setCount] = React.useState(0)
const handleClick = React.useCallback(
() => console.log('clicked!'),
[]) // useCallback 사용
return (
<>
<button onClick={() => setCount(count + 1)}>카운트 올리기</button>
<button onClick={handleClick}>클릭해보세요!</button>
</>
)
}
위 코드에선 useCallback
으로 감싸기만 했을 뿐인데, 이전에 생성한 함수를 저장해두고 재사용합니다. 함수의 동작은 똑같지만 좀 더 최적화가 좋습니다. 이는 메모제이션 패턴을 이용한 것입니다. 잠깐, 그런데 두번째 인자로 넘긴 []
은 무엇일까요?
두번째 인자의 배열은 의존성을 의미합니다. 여태 작성한 handleClick
함수는 아무런 의존성이 없기에 문제가 되지 않습니다. 코드를 조금 변경해서 handleClick
함수가 count
값을 출력하게 해보겠습니다.
const handleClick = React.useCallback(
() => console.log('current count :' + count),
[])
다음과 같은 순서로 이벤트를 발생시켜보겠습니다.
출력 결과
handleClick() // 실제 count 값: 0, 출력 결과: current count :0
setCount(count + 1) // 실제 count 값: 1
handleClick() // 실제 count 값: 1, 출력 결과: current count :0
두번째 호출에서 실제 count 값이 1증가해서 변경되었음에도 최초값인 0을 출력합니다. useCallback
내부에서 count
값을 의존하지만, 이를 제대로 인지하지 못하고 이전 값을 출력하는 것입니다.
따라서 다음과 같이 useCallback
의 두번째 인자 배열에 의존하는 상태값을 명시해야 제대로 동작합니다.
const handleClick = React.useCallback(
() => console.log('current count :' + count),
[count]) // 의존하는 상태 명시
출력 결과
handleClick() // 실제 count 값: 0, 출력 결과: current count :0
setCount(count + 1) // 실제 count 값: 1
handleClick() // 실제 count 값: 1, 출력 결과: current count :1
이처럼 useCallback
함수 내부에서 의존하는 상태값이 있다면, 반드시 두번째 인자 배열에 명시해야합니다.
useMemo
또한 useCallback
과 매우 유사하게 최적화에 사용됩니다. useCallback
이 함수를 반환하는 반면, 이것은 값을 반환합니다. count
값의 두배를 계산한 상태를 예시로 들어보겠습니다.
function Component() {
const [count, setCount] = React.useState(0)
const doubleCount = count * 2
console.log(doubleCount) // 두배로 계산한 값 출력
return (
<>
<button onClick={() => setCount(count + 1)}>카운트 올리기</button>
</>
)
}
버튼을 클릭할 때 마다 두배로 계산한 값을 출력합니다. 하지만 count값과 무관하게 컴포넌트가 재렌더링 되었을 경우, 불필요한 연산을 하게 됩니다. 컴포넌트의 상태값이 많고 복잡한 연산의 경우 최적화가 좋지 않습니다.
const doubleCount = React.useMemo(() => count * 2, [count])
위처럼 useMemo
를 사용하면 의존하는 값이 변경될 때에만 연산하므로 최적화가 개선됩니다. useCallback
과 마찬가지로 두번째 인자 배열에 의존하는 값을 반드시 명시해야합니다.
참고로 이전에 useCallback
으로 작성한 handleClick
함수를 useMemo
를 사용해서 똑같이 구현할 수 있습니다. 내부에서 함수만 반환하게 하면 됩니다.
const handleClick = React.useMemo(
() => () => console.log('current count :' + count),
[count]) // useMemo로 useCallback 구현
useMemo
는 상태값을 반환하고, useCallback
은 함수를 반환하는 차이를 제외하곤 없습니다. 이를 적절히 사용하면 컴포넌트 렌더링 최적화에 큰 도움이 될 수 있다고 생각합니다. 한가지 주의할 점은 useCallback
과 useMemo
를 무분별하게 사용한다면, 이를 사용하는 코드와 메모제이션용 메모리가 추가로 필요하게 되므로 적절하게 사용해야합니다.
유용한 글 감사합니다! 이 글 덕분에 헷갈렸던 useCallback, useMemo가 이해되었습니다 :)