지난번에 이어서 ReactJS 공식 문서 읽기!!! 그럼 바로 스타트~
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
기본적으로 Effect 는 모든 렌더링 후 실행되므로 렌더링의 결과로 실행이 된다. 그리고 state를 설정하면 렌더링이 트리거된다.
따라서 Effect가 실행되고 상태가 설정되면 리렌더링이 발생하고 Effect가 다시 실행되기 때문에 무한루프에 빠지게 된다.
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);
위의 코드를 보면 의존성 배열에 추가된 건 isPlaying 뿐이다. useRef 호출 시 항상 같은 객체를 얻는다는 안전된 식별성을 가지기 때문에 ref는 포함하지 않는 것이다.
ref 객체는 절대 변경되지 않고 Effect를 다시 실행시키지 않으므로 의존성 배열에 ref 객체가 포함되어 있어도 상관은 없다.
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
빈배열로 의존성 배열을 처리하면 된다.
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ todos나 filter가 변경되지 않는 한 getFilteredTodos()를 다시 실행하지 않습니다.
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}
useMemo hook으로 래핑해서 값비싼 계산을 메모이제이션할 수 있다. useMemo는 첫번째 렌더링을 빠르게 만드는 데 도움이 되는 것이 아니라 업데이트 시 불필요한 작업을 건너뛸 때만 도움이 된다.
그리고 계산이 비싼지 알려면 다음과 같이 측정하여 확인하면 된다.
console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');
의존성 배열을 effect의 종속성으로 지정하여 다시 동기화를 한다.
function ChatRoom({ roomId }) { // roomId prop은 시간이 지남에 따라 변경될 수 있습니다.
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 이 effect는 roomId를 읽습니다.
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // 따라서 React에 이 effect가 roomId에 "의존"한다고 알려줍니다.
// ...
roomId 가 general 이었다가 다음 렌더링 중 travel을 전달한 경우 Object.is로 비교하여 React는 effect를 다시 동기화한다. 반면 컴포넌트가 다시 렌더링되지만 roomId가 변경되지 않은 경우 effect는 동일한 roomId에 연결된 상태를 유지한다.
위의 말을 다시하자면 의존성 배열에는 변경이 감지될 수 있는 값이여야 useEffect가 동작한다는 의미이다.
React의 의존성 배열은 값의 참조에 의존하기 때문에 값 자체가 변경되더라도 참조가 동일하다면 useEffect는 다시 실행되지 않을 수 있다.
React 18에서 등장한 기능으로 react 컴포넌트가 렌더링될 떄 참조 무결성을 보장하면서 useEffect 안에서 이벤트 핸들러를 선언하고 사용할 수 있도록 도와준다.
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = () => {
console.log(count);
};
window.addEventListener("click", handleClick);
return () => window.removeEventListener("click", handleClick);
}, [count]); // count가 바뀔 때마다 useEffect가 재실행됨
return <button onClick={() => setCount(count + 1)}>Increment</button>;
}
위와 같은 코드에서 count 가 변경될 때마다 새로운 handleClick 함수가 생성되며 그로 인해 이벤트 리스너도 새로 등록된다. 이렇게 되면 useEffect가 의도치않게 자주 호출되는 문제가 발생하여 useEffectEvent가 도입되었다.
import { useEffectEvent } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = useEffectEvent(() => {
console.log(count);
});
useEffect(() => {
window.addEventListener("click", handleClick);
return () => window.removeEventListener("click", handleClick);
}, [handleClick]); // handleClick은 불변 함수가 되어 다시 호출되지 않음
return <button onClick={() => setCount(count + 1)}>Increment</button>;
}
위의 코드처럼 이벤트 핸들러를 useEffectEvent 로 감싸서 렌더링이 다시 일어나더라도 참조가 동일한 핸들러를 유지하도록 한다.
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...
<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>
위와 같은 코드는 부모 컴포넌트가 재렌더링할 때마다 Effect가 다시 연결되는 문제점이 있다. 이런 문제점을 해결하려면 비구조할당으로 props 를 처리해야한다.
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...
import { use } from 'react'; // 아직 사용 불가능합니다!
function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities?country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...
react 공식 문서에 의하면 위와 같은 코드처럼 data fetching 이 이뤄지도록 수정된다고 한다..!!!
컴포넌트 내부에서 선언된 Props, state 및 기타 값은 렌더링 중에 계산되고 React 데이터 흐름에 참여한다.
따라서 이런 반응형 데이터는 종속성배열에 포함되어야 한다.
useState에서 반환되는 set함수와 useRef에서 반환되는 ref 객체는 다시 렌더링해도 변경되지 않도록 보장한다. 따라서 이런 값들은 의존성 배열에 포함하지 않아도 된다.
custom hook을 만들 때, 항상 use로 시작해야한다. use로 시작하지 않으면 linter가 내부에서 useState나 useEffect를 사용하는 것을 허용하지 않는다. 즉 Hook을 호출히는 custom hook은 use로 시작해야한다.
커스텀 Hook은 state 자체가 아닌 state 저장 로직만 공유한다.
모든 Hook은 컴포넌트가 재렌더링될 때마다 재실행된다.
커스텀 Hook을 통해 받는 이벤트 핸들러는 Effect로 감싸야한다.