함수 실행 시 함수 외부의 상태를 변경하는 연산은 부수 효과라고 한다. 대부분의 부수 효과는 useEffect 훅에서 처리하는게 좋다. API를 호출하거나 이벤트 처리 함수를 등록하고 해제하는 것 등이 부수 효과의 예이다.
(리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정)
useEffect 훅에 입력하는 함수를 부수 효과 함수라고 한다. 부수 효과 함수는 렌더링 결과가 실제 돔에 반영된 후 호출되고, 컴포넌트가 사라지기 전에 마지막으로 호출된다.
(부수 효과 함수는 렌더링 결과가 실제 돔에 반영된 후에 비동기적으로 실행 됨.)
// 처음 마운트되고 실행
useEffect(() => {
console.log('컴포넌트가 화면에 나타남');
}, []);
부수 효과 함수는 렌더링할 때마다 호출되기 때문에 불필요한 작업을 반복적으로 할 수 있다. 이를 방지하기 위해 useEffect 두번째 매개변수로 의존성 배열에 값을 입력합니다. 그러면 컴포넌트가 처음 마운트 될때에도 호출이 되고, 지정한 값이 변경될 때마다 호출됩니다.
// 특정 상태나 값이 업데이트 될 때만 실행
useEffect(() => {
console.log('컴포넌트가 화면에 나타남');
}, [value]);
이벤트 처리함수 등록하고 해제하기
부수 효과 함수는 함수를 반환 할 수 있다. 반환한 함수는 cleanup이라고 부른다.
반환된 함수는 부수 효과 함수가 호출되기 직전에 호출되고, 컴포넌트가 사라지기 직전에 마지막으로 호출된다. 의존성 배열로 []을 주면 컴포넌트가 생성될 때만 부수 효과 함수가 호출되고, 컴포넌트가 사라 질 때만 반환된 함수가 호출된다.
// 뒷 정리
useEffect(() => {
window.addEventListener('scroll', cb);
console.log('컴포넌트가 화면에 나타남');
return () => {
window.removeEventListener('scroll', cb);
console.log('컴포넌트가 화면에 사라짐');
};
}, []);
컴포넌트에서 DOM 요소를 선택해야 할 때 ref
를 사용한다. 함수형 컴포넌트에서는 ref
를 사용할 때에는 useRef 훅을 사용한다.
useRef로 관리하는 변수는 값이 변경되어도 컴포넌트에서 리렌더링되지 않는다 . 이런 특성을 활용해
컴포넌트에서 생성된 값 중에는 렌더링과 무관한 값을 useRef 훅을 사용하여 ref에 저장하여 사용한다.
ex) setTimeout이 반환한 값을 저장해두고 clearTimeout을 호출 시 사용
useRef를 사용하여 이전 상태 값 저장하기
function Profile() {
const [age, setAge] = useState(0);
const prevAgeRef = useRef(20);
// age 값이 변경되면 그 값을 prevAgeRef에 저장
useEffect(() => {
prevAgeRef.current = age;
}, [age]);
const prevAge = prevAgeRef.current; // 이전 상태값 저장
const text = age === prevAge ? 'same' : age > prevAge ? 'older' : 'younger';
return (
<div>
<p>
age {age} is {text} age {prevAge}
</p>
<button
onClick={() => {
const age = Math.floor(Math.random() * 50 + 1);
setAge(age); // age가 변경되어 다시 렌더링
}}
>
나이 변경
</button>
</div>
);
}
useState 훅도 이전 상태 저장이 가능하지만 컴포넌트의 생병주기와 밀접하게 연과되어 있기 때문에 렌더링과 무관한 값을 저장하기에는 적합하지 않다.
useMemo와 useCallback은 이전 값을 기억해서 성능을 최적화하는 용도로 사용된다.
useMemo는 계산량이 많은 함수의 반환값을 재활용하는 용도로 사용된다.
import React, { useMemo } from 'react';
function component({ v1, v2 }) {
const value = useMemo(() => 계산함수(v1, v2), [v1, v2]);
return <p>{value}</p>;
}
useMemo는 첫번째 인자로 함수, 두번째 인자로 의존성 배열을 받는다. 첫번째 함수가 반환한 값을 기억하고 의존성 배열이 변경되지 않으면 이전에 반환한 값을 재사용하고, 만약 배열의 값이 변경되었을 경우 함수를 재실행하고 반환 값을 기억한다.
useCallback은 리액트의 렌더링 성능을 위한 훅이다.
function Profile() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
return (
<div>
<p>name is {name}</p>
<p>age is {age}</p>
<UserEdit
onSave={() => saveToServer(name, age)}
setName={setName}
setAge={setAge}
/>
</div>
);
}
Profile 컴포넌트는 렌더링될 때마다 UserEdit의 onSave 속성값으로 새로운 함수를 생성한다. 그래서 UserEdit 컴포넌트에서 useMemo를 사용하더라도 onSave 속성 값이 계속 변경되기 때문에 불필요한 렌더링이 발생한다. onSave 속성 값은 name, age 값이 같으면 항상 같아야한다.
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>
);
}
이전에 onSave 속성 값에 전달했던 함수를 useCallback의 첫번째 매개변수로 입력하고, 두번째 매개변수는 의존성 배열이다. 의존성 배열이 변경되지 않으면 이전에 생성한 함수를 그대로 사용한다. 즉, name과 age값이 변경되지 않으면 UserEdit 컴포넌트의 onSave 속성 값으로 항상 같은 함수가 전달된다.
useReducer 훅을 사용하면 컴포넌트의 상태 값을 리덕스의 리듀서처럼 관리할 수 있다.
import React, { useReducer } from 'react';
const INITIAL_STATE = { name: 'kang', age: 27 };
// 리듀서 함수
function reducer(state, action) {
switch (action.type) {
case 'setName':
return { ...state, name: action.name };
case 'setAge':
return { ...state, age: action.age };
default:
return state;
}
}
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>
);
}
dispatch는 리덕스의 dispatch 함수와 같은 방식으로 작동한다. action type에 해당하는 경우를 수행한다.
보통 상위 컴포넌트에서 다수의 상태 값을 관리한다. 이때 하위 컴포넌트로부터 발생한 이벤트에서 상위 컴포넌트의 상태 값을 변경해야하는 경우가 많다. 이를 위해 상위 컴포넌트에서 트리의 깊은 곳까지 이벤트 처리 함수를 전달한다.
이 경우에 useReducer 훅과 Context API 이용하면 쉽게 전달이 가능하다.
// ...
export const ProfileDispatch = React.createContext(null);
// ...
function Profile() {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
return (
<div>
<p>name is {state.name}</p>
<p>age is {state.age}</p>
<ProfileDispatch.Provider value={dispatch}>
<ChildComponent />
</ProfileDispatch.Provider>
</div>
);
}
useImperativeHandle를 사용하면 부모 컴포넌트에서 접근 가능한 함수를 만들 수 있습니다.
useImperativeHandle로 외부로 공개할 함수 정의
import React, { forwardRef, useState, useImperativeHandle } from 'react';
function Profile(props, ref) {
const [name, setName] = useState('');
const [age, setAge] = useState('');
// ref 객체와 부모 컴포넌트에서 접근 가능한 여러 함수 작성
useImperativeHandle(ref, () => ({
addAge: (value) => setAge(age + value),
getNameLength: () => name.length,
}));
return (
<div>
<p>name is {state.name}</p>
<p>age is {state.age}</p>
</div>
);
}
export default forwardRef(Profile);
useImperativeHandle 훅으로 정의한 함수를 외부에서 호출
function Parent() {
const profileRef = React.useRef();
const onClick = () => {
if (profileRef.current) {
console.log('current name len:', profileRef.current.getNameLength());
profileRef.current.addAge(5);
}
};
return (
<div>
<Profile ref={profileRef} />
<button onClick={onClick}>add age 5</button>
</div>
);
}
useEffect 훅에 입력된 부수 효과 함수는 렌더링 결과가 돔에 반영된 후에 비동기로 호출된다.
반면에 useLayoutEffect 훅은 useEffect와 비슷하게 동작하지만 부수 효과 함수를 동기로 호출한다. 즉, useLayoutEffect 훅의 부수 효과 함수는 렌더링 결과가 돔에 반영된 직후에 호출된다.
useLayoutEffect 훅의 부수 효과 함수에서 연산을 많이 하면 브라우저가 먹통이 될 수 있으므로
렌더링 직후 돔 요소 값을 읽는 경우에는 useLayoutEffect를 사용하고 다른 경우에는 useEffect 훅을 사용하는게 성능상 좋다.
useDebugValue 개발 편의를 위해 제공하는 훅이다. useDebugValue 훅을 사용하면 커스텀 훅의 내부 상태를 관찰할 수 있기 때문에 디버깅에 도움이 된다.