컴포넌트 중 일부는 React 외부의 시스템을 제어하고 동기화해야 할 수 있습니다. 예를 들어, 브라우저 API를 통해 input에 초점을 맞추거나, REACT 없이 구현된 비디오 플레이어를 재생 및 일시정지하거나, 원격 서버에 연결해서 메시지를 수신해야 할 수 있습니다. 이 장에서는 React의 “외부”로 나가서 외부 시스템에 연결할 수 있는 탈출구를 배우게 됩니다. 대부분의 애플리케이션 로직과 데이터 흐름은 이러한 기능에 의존해서는 안됩니다.
컴포넌트가 특정 정보를 ‘기억’하도록 하고 싶지만 해당 정보가 리렌더링을 촉발하지 않도록 하려는 경우 ref를 사용할 수 있습니다.
const ref = useRef(0);
상태와 마찬가지로 ref는 리렌더링 사이에 React에 의해 유지됩니다. 다만 상태를 설정하면 컴포넌트가 다시 렌더링되는 반면, ref를 변경하면 그렇지 않습니다. ref.current 프로퍼티를 통해 해당 ref의 현재 값에 접근할 수 있습니다.
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
}
return (
<button onClick={handleClick}>
Click me!
</button>
);
}
ref는 React가 추적하지 않는 컴포넌트의 비밀 주머니와 같습니다. 예를 들어, ref를 사용하여 컴포넌트의 렌더링 출력에 영향을 주지 않는 timeout ID, DOM 엘리먼트 및 기타 객체를 저장할 수 있습니다.
React는 렌더링 출력과 일치하도록 DOM을 자동으로 업데이트하므로 컴포넌트에서 자주 조작할 필요가 없습니다. 하지만 때로는 노드에 초점을 맞추거나 스크롤하거나 크기와 위치를 측정하기 위해 React가 관리하는 DOM 요소에 접근해야 할 수 있습니다. React에는 이러한 작업을 수행할 수 있는 빌트인된 방법이 없으므로 DOM 노드에 대한 ref(참조)가 필요합니다. 예를 들어, 버튼을 클릭하면 ref를 사용해 input에 초점을 맞춥니다 :
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
일부 컴포넌트는 외부 시스템과 동기화해야 합니다. 예를 들어, React 상태에 따라 비 React 컴포넌트를 제어하거나, 서버 연결을 설정하거나, 컴포넌트가 화면에 표시될 때 분석 로그를 보내야 할 수 있습니다. 특정 이벤트를 처리할 수 있는 이벤트 핸들러와 달리 Effect를 사용하면 렌더링 후 일부 코드를 실행할 수 있습니다. 이를 사용해 컴포넌트를 React 외부 시스템과 동기화할 수 있습니다.
재생/일시정지를 몇 번 누르고 비디오 플레이어가 isPlaying prop 값에 어떻게 동기화되는지 확인해 보세요:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
많은 Effect는 스스로 “정리”하기도 합니다. 예를 들어, 채팅 서버에 대한 연결을 설정하는 Effect는 해당 서버에 컴포넌트의 연결을 끊는 방법을 React에 알려주는 클린업 함수를 반환합니다.
// App.js
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, []);
return <h1>Welcome to the chat!</h1>;
}
// chat.js
export function createConnection() {
// A real implementation would actually connect to the server
return {
connect() {
console.log('✅ Connecting...');
},
disconnect() {
console.log('❌ Disconnected.');
}
};
}
개발 모드에서는 React는 즉시 실행되고 Effect를 한 번 더 정리합니다. 그래서 "✅Connecting…" 이 두 번 프린트되는 것입니다. 이렇게 하면 클린업 함수를 구현하는 것을 잊지 않도록 할 수 있습니다.
Effect는 React 패러다임에서 벗어날 수 있는 탈출구입니다. 이를 통해 React의 “외부”로 나가서 컴포넌트를 외부 시스템과 동기화할 수 있습니다. 외부 시스템이 관여하지 않는 경우(예: 일부 prop이나 상태가 변경될 때 컴포넌트의 상태를 업데이트하려는 경우)에는 Effect가 필요하지 않습니다. 불필요한 Effect를 제거하면 코드를 더 쉽게 따라갈 수 있고, 실행 속도가 빨라지며, 오류 발생 가능성이 줄어듭니다.
Effect가 필요하지 않은 일반적인 경우는 두 가지가 있습니다:
- 렌더링을 위해 데이터를 변환할 때 Effect는 필요하지 않습니다.
- 사용자 이벤트를 처리할 때 Effect는 필요하지 않습니다.
예를 들어, 다른 상태에 따라 일부 상태를 조정하는 데는 Effect가 필요하지 않습니다:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// 아래 코드로 대체
const fullName = firstName + ' ' + lastName;
}
하지반 외부 시스템과 동기화하려면 Effects가 필요할 것입니다.
Effect는 컴포넌트와 다른 생명주기를 가집니다. 컴포넌트는 마운트, 업데이트 또는 언마운트할 수 있습니다. 반면 Effect는 동기화를 시작하거나 동기화를 중지하는 두 가지 작업만 할 수 있습니다. Effect가 시간에 따라 변하는 prop 및 상태에 의존하는 경우 여러 번 발생할 수 있습니다.
아래 예시는 Effect는 roomId prop의 값에 따라 달라집니다. prop은 반응형 값이므로 리렌더링할 때 변경될 수 있습니다. roomId 가 변경되면 Effect가 재동기화(및 서버 재연결)됩니다.
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
<useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h1>Welcome to the {roomId} room!</h1>;
}
export default function App() {
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
Choose the chat room:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">general</option>
<option value="travel">travel</option>
<option value="music">music</option>
</select>
</label>
<hr />
<ChatRoom roomId={roomId} />
</>
);
}
React는 Effect의 의존성을 올바르게 지정했는지 확인하는 린터 규칙을 제공합니다. 위의 예시에서 의존성 목록에 roomId 를 지정하는 것을 잊어버렸다면, 린터가 해당 버글르 자동으로 찾아낼 것입니다.
이 섹션에서는 아직 안정된 버전의 React로 출시되지 않은 실험적인 API에 대해 설명합니다.
이벤트 핸들러는 동일한 상호작용을 다시 수행할 때만 다시 실행됩니다. 이벤트 핸들러와 달리 Effect는 prop이나 상태 변수처럼 읽은 값이 마지막 렌더링에서와 다른 경우 다시 동기화합니다. 때로는 두 가지 동작을 혼합하여 일부 값에는 반응하지만 다른 값에는 반응하지 않는 Effect를 원할 수 있습니다.
Effect 내의 모든 코드는 반응형이며, 읽은 반응형 값이 리렌더링으로 인해 변경된 경우 다시 실행됩니다. 예를 들어, Effect는 상호작용 후 roomId 또는 theme 이 변경된 경우 채팅에 다시 연결됩니다:
// App.js
import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]);
return <h1>Welcome to the {roomId} room!</h1>
}
export default function App() {
const [roomId, setRoomId] = useState('general');
const [isDark, setIsDark] = useState(false);
return (
<>
<label>
Choose the chat room:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">general</option>
<option value="travel">travel</option>
<option value="music">music</option>
</select>
</label>
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Use dark theme
</label>
<hr />
<ChatRoom
roomId={roomId}
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
// chat.js
export function createConnection(serverUrl, roomId) {
// A real implementation would actually connect to the server
let connectedCallback;
let timeout;
return {
connect() {
timeout = setTimeout(() => {
if (connectedCallback) {
connectedCallback();
}
}, 100);
},
on(event, callback) {
if (connectedCallback) {
throw Error('Cannot add the handler twice.');
}
if (event !== 'connected') {
throw Error('Only "connected" event is supported.');
}
connectedCallback = callback;
},
disconnect() {
clearTimeout(timeout);
}
};
}
// notifications.js
import Toastify from 'toastify-js';
import 'toastify-js/src/toastify.css';
export function showNotification(message, theme) {
Toastify({
text: message,
duration: 2000,
gravity: 'top',
position: 'right',
style: {
background: theme === 'dark' ? 'black' : 'white',
color: theme === 'dark' ? 'white' : 'black',
},
}).showToast();
}
이것은 이상적이지 않습니다. roomId 가 변경된 경우에만 채팅에 다시 연결하고 싶습니다. theme 를 전환해도 채팅에 다시 연결되지 않아야 합니다! theme 를 읽는 코드를 Effect에서 Effect Event로 옮기세요.
// App.js
import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h1>Welcome to the {roomId} room!</h1>
}
export default function App() {
const [roomId, setRoomId] = useState('general');
const [isDark, setIsDark] = useState(false);
return (
<>
<label>
Choose the chat room:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">general</option>
<option value="travel">travel</option>
<option value="music">music</option>
</select>
</label>
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Use dark theme
</label>
<hr />
<ChatRoom
roomId={roomId}
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
이벤트 함수 내부의 코드는 반응형 코드가 아니므로 theme 를 변경해도 더 이상 Effect가 다시 연결되지 않습니다.
Effect를 작성할 때, Effect의 의존성 목록에서 Effect가 읽는 모든 반응형 값(예: prop 및 상태)을 포함했는지 확인합니다. 이렇게 하면 Effect가 컴포넌트의 최신 prop 및 상태와 동기화된 상태를 유지할 수 있습니다. 불필요한 의존성으로 인해 Effect가 너무 자주 실행되거나 무한 루프를 생성할 수도 있습니다. 의존성을 제거하는 방법은 경우에 따라 다릅니다.
예를 들어, 이 Effect는 사용자가 input을 편집할 때마다 다시 생성되는 options 객체에 의존합니다:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = {
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]);
return (
<>
<h1>Welcome to the {roomId} room!</h1>
<input value={message} onChange={e => setMessage(e.target.value)} />
</>
);
}
export default function App() {
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
Choose the chat room:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">general</option>
<option value="travel">travel</option>
<option value="music">music</option>
</select>
</label>
<hr />
<ChatRoom roomId={roomId} />
</>
);
}
// chat.js
export function createConnection({ serverUrl, roomId }) {
// A real implementation would actually connect to the server
return {
connect() {
console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
},
disconnect() {
console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
}
};
}
해당 채팅에 메시지를 입력할 때마다 채팅이 다시 연결되는 것을 원치 않으실 것입니다. 이 문제를 해결하려면 Effect 내에서 options 객체를 생성하여 Effect가 roomId 문자열에만 의존하도록 하세요:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return (
<>
<h1>Welcome to the {roomId} room!</h1>
<input value={message} onChange={e => setMessage(e.target.value)} />
</>
);
}
export default function App() {
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
Choose the chat room:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">general</option>
<option value="travel">travel</option>
<option value="music">music</option>
</select>
</label>
<hr />
<ChatRoom roomId={roomId} />
</>
);
}
의존성 목록을 편집하여 options 의존성을 제거하는 것으로 시작하지 않았음을 알 수 있습니다. 이는 잘못된 방법일 수 있습니다. 대신 주변 코드를 변경함으로써 의존성을 불필요하게 만들었습니다. 의존성 목록을 Effect의 코드에서 사용하는 모든 반응형 값의 목록으로 생각하세요. 이 목록에 무엇을 넣을지는 의도적으로 선택하지 않습니다. 이 목록은 당신의 코드를 기술합니다. 의존성 목록을 변경하려면, 코드를 변경하세요.
React에는 useState , useContext , useEffect 와 같은 훅이 빌트인 되어 있습니다. 때로는 데이터를 페치하거나, 사용자가 온라인 상태인지 추적하거나, 채팅방에 연결하는 것과 가팅 좀 더 구체적인 목적을 위한 훅이 있었으면 좋겠습니다. 이를 위해 애플리케이션의 필요에 맞는 고유한 훅을 만들 수 있습니다.
이번 예제에서는 usePointerPositon 커스텀 훅은 커서 위치를 추적하고, useDelayedValue 커스텀 훅은 전달한 값보다 특정 밀리초만큼 ‘지연’된 값을 반환합니다. 샌드박스 미리보기 영역 위로 커서를 이동하면 커서를 따라 움직이는 점의 흔적을 확인할 수 있습니다.
// App.js
import { usePointerPosition } from './usePointerPosition.js';
import { useDelayedValue } from './useDelayedValue.js';
export default function Canvas() {
const pos1 = usePointerPosition();
const pos2 = useDelayedValue(pos1, 100);
const pos3 = useDelayedValue(pos2, 200);
const pos4 = useDelayedValue(pos3, 100);
const pos5 = useDelayedValue(pos4, 50);
return (
<>
<Dot position={pos1} opacity={1} />
<Dot position={pos2} opacity={0.8} />
<Dot position={pos3} opacity={0.6} />
<Dot position={pos4} opacity={0.4} />
<Dot position={pos5} opacity={0.2} />
</>
);
}
function Dot({ position, opacity }) {
return (
<div style={{
position: 'absolute',
backgroundColor: 'pink',
borderRadius: '50%',
opacity,
transform: `translate(${position.x}px, ${position.y}px)`,
pointerEvents: 'none',
left: -20,
top: -20,
width: 40,
height: 40,
}} />
);
}
// usePointerPosion.js
import { useState, useEffect } from 'react';
export function usePointerPosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
function handleMove(e) {
setPosition({ x: e.clientX, y: e.clientY });
}
window.addEventListener('pointermove', handleMove);
return () => window.removeEventListener('pointermove', handleMove);
}, []);
return position;
}
// useDelayedValue.js
import { useState, useEffect } from 'react';
export function useDelayedValue(value, delay) {
const [delayedValue, setDelayedValue] = useState(value);
useEffect(() => {
setTimeout(() => {
setDelayedValue(value);
}, delay);
}, [value, delay]);
return delayedValue;
}
커스텀 훅을 생성하고, 함께 구성하고, 컴포넌트 간에 데이터를 전달하고, 컴포넌트 간에 재사용할 수 있습니다. 앱이 성장함에 따라 이미 작성한 커스텀 훅을 재사용할 수 있기 때문에 직접 작성하는 Effect의 수가 줄어들게 됩니다. 또한 React 커뮤니티에서 관리하고 있는 훌륭한 커스텀 훅이 많이 있습니다.