일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 타입스크립트
- typeScript
- Next 팀 프로젝트
- 파이썬 replace
- 파이썬 for
- 파이썬 딕셔너리
- 리액트 프로젝트
- tanstack query
- 타입스크립트 props
- JavaScript
- 내배캠 프로젝트
- 코딩테스트
- 파이썬 for in
- 리액트 공식문서
- 파이썬 enumerate
- 프로그래머스
- 리액트
- 한글 공부 사이트
- 리액트 훅
- React Hooks
- useEffect
- 내일배움캠프 프로젝트
- 타입스크립트 리액트
- 파이썬 반복문
- REACT
- 내일배움캠프
- 자바스크립트
- 내일배움캠프 최종 프로젝트
- useState
- 파이썬 slice
- Today
- Total
sohyeon kim
[React] 리액트 공식문서 정리 4(3) : 탈출구 - Effect 생명주기, 동기화, 의존성 배열, useEffect VS 이벤트 핸들러, useEffectEvent 본문
[React] 리액트 공식문서 정리 4(3) : 탈출구 - Effect 생명주기, 동기화, 의존성 배열, useEffect VS 이벤트 핸들러, useEffectEvent
aotoyae 2024. 12. 6. 14:44
💡 리액트 공식문서 정리 : 탈출구 - 반응형 Effect 의 생명주기, Effect 에서 이벤트 분리하기
1. 반응형 Effect 의 생명주기 : 동기화 시작 & 중지
Effect 는 컴포넌트와 다른 생명주기를 가진다. 컴포넌트는 마운트/업데이트/마운트 해제의 생명주기를 가지지만,
Effect 는 동기화를 시작하거나 중지하는 두 가지 작업만 할 수 있다.
다음 Effect 는 roomId prop 값에 의존한다. roomId 가 변경되면 Effect 가 다시 동기화(및 서버에 다시 연결)한다.
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 동기화 시작
connection.connect();
return () => connection.disconnect(); // 동기화 중지(cleanup 함수)
}, [roomId]);
return <h1>Welcome to the {roomId} room!</h1>;
}
⚠️ 컴포넌트가 마운트될 때 동기화를 시작하고, 마운트 해제될 대 동기화를 중지할 것이라 생각할 수도 있다.
하지만 때로는 마운트된 상태에서 동기화를 여러 번 시작하고 중지해야 할 수도 있다.
이러한 동작이 필요한 이유와 발생 시기, 그리고 이러한 동작의 제어 방법에 대해 알아보자.
❗️참고❗️ 일부 effet 들은 cleanup 함수를 전혀 반환하지 않는다. 그럼 React 는 빈 cleanup 함수를 반환한 것처럼 동작한다.
1️⃣ 동기화가 두 번 이상 수행되어야 하는 이유
ChatRoom 컴포넌트가 사용자가 드롭다운에서 선택한 roomId prop 을 받는다고 가정해 보자.
처음 사용자가 'general' 대화방을 roomId 로 선택하면, 앱에 'general' 채팅방이 표시된다.
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}
UI 가 표시되면 React 가 effect 를 실행해 동기화를 시작하고, 'general 방에 연결된다.
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // "general" 방에 연결
connection.connect();
return () => {
connection.disconnect(); // "general" 방에서 연결 해제
};
}, [roomId]);
// ...
지금까진 괜찮다.
이후 사용자가 드롭다운에서 다른 방을 선택하고, React 는 먼저 UI 를 업데이트한다.
function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}
사용자는 UI 를 보고 'travel' 이 선택된 대화방임을 알 수 있다. 그러나 지난번에 실행된 effect 는 여전히 'general' 방에 연결되어 있다.
roomId prop 이 변경되었기 때문에 그 때 effect 가 수행한 작업('general' 방에 연결)이 현재 UI 와 일치하지 않게 된 것.
이 시점에서 React 는 두 가지 작업을 수행해야 한다.
- 이전 roomId 와의 동기화 중지('general' 방에서 연결 끊기)
- 새 roomId 와 동기화 시작('travel' 방에 연결)
다행히, 우린 이미 위 수행 방법을 React 에 알려줬다. effect 의 본문엔 동기화 시작 방법이 명시되어 있고,
cleanup 함수엔 동기화 중지 방법이 명시되어 있다. 이제 React 는 올바른 순서로 올바른 props 와 state 로 호출하기만 하면 된다.
2️⃣ React 가 effect 를 재동기화하는 방법
컴포넌트가 roomId prop 에 새로운 값('travel')을 받은 상태이고, 다른 방에 재연결하려면 React 가 effect 를 다시 동기화해야 한다.
동기화를 중지하기 위해 React 는 'general' 방에 연결한 후 effect 가 반환한 cleanup 함수를 호출한다.
roomId 가 'general' 이었기 때문에, cleanup 함수는 'general' 방에서 연결을 끊는다.
return () => {
connection.disconnect(); // "general" 방에서 연결 해제
};
이제 React 는 렌더링 중에 우리가 제공한 effect 를 실행한다. 이번 roomId 인 'travel' 채팅방과 동기화가 시작된다.
function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // "travel" 방에 연결
connection.connect();
// ...
이제 UI 에서 선택한 방과 동일한 방에 연결되었다! 또한, 컴포넌트가 다른 roomId 로 다시 렌더링할 때마다 effect 가 다시 동기화된다.
예시로 'travel' 에서 'music' 으로 변경한다면?
- React 는 다시 cleanup 함수를 호출해 effect 동기화를 중지('travel' 방에서 연결 해제)한다.
- 그 다음 새 roomId 로 본문을 실행해 동기화를 다시 시작('music' 방에 연결)한다.
- 마지막으로 사용자가 다른 화면으로 이동하면 ChatRoom 이 마운트를 해제한다.
- 이제 연결 상태를 유지할 필요가 없고, React 는 마지막으로 effect 동기화를 중지('music'에서 연결 해제)한다.
3️⃣ effect 의 관점에서 생각하기
ChatRoom 컴포넌트의 관점에서 일어난 일들을 요약해 보자.
- roomId 가 'general' 로 설정되어 마운트된 ChatRoom
- roomId 가 'travel' 로 설정되어 업데이트된 ChatRoom
- roomId 가 'music' 로 설정되어 업데이트된 ChatRoom
- 마운트 해제된 ChatRoom
컴포넌트의 생명주기에서 이러한 각 시점에서 effect 는 다른 작업을 수행했다.
- effect 가 'general' 방에 연결됨
- 'general' 방에서 연결이 끊어지고 'travel' 방에 연결된 effect
- 'travel' 방에서 연결이 끊어지고 'music' 방에 연결된 effect
- 'music' 방에서 연결이 끊어진 effect
이제 effect 자체의 관점에서 무슨 일이 일어났는지 살펴 보자.
useEffect(() => {
// roomId로 지정된 방에 연결된 effect...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...연결이 끊어질 때까지
connection.disconnect();
};
}, [roomId]);
이 코드 구조는 어떤 일이 일어났는지, 겹치지 않는 시간의 연속으로 볼 수 있다.
- 'general' 방에 연결된 effect (연결이 끊어질 때까지)
- 'travel' 방에 연결된 effect (연결이 끊어질 때까지)
- 'music' 방에 연결된 effect (연결이 끊어질 때까지)
이전엔 컴포넌트의 관점에서 생각했으니, effect 를 '렌더링 후' 또는 '마운트 해제 전' 과 같은 특정 시점에 실행되는 '콜백' 또는 '생명주기 이벤트' 로 생각하기 쉬웠다. 이러한 사고는 매우 복잡해지므로 피하는 것이 좋다.
대신 항상 한 번에 하나의 시작/중지 사이클에만 집중하자. 컴포넌트를 마운트, 업데이트 또는 마운트 해제하는 것은 중요하지 않다.
동기화 시작 방법과 중지 방법만 설명하면 된다. 이 작업을 잘 수행하면 필요 횟수만큼 effect 를 시작하고 중지할 수 있다.
4️⃣ 각 effect 는 별도의 동기화를 프로세스를 나타낸다.
위 로직은 이미 작성한 effect 와 동시에 실행되어야 하므로 관련 없는 로직을 effect 에 추가하지 말자.
예시로 사용자가 회의실을 방문할 때 분석 이벤트를 전송하고 싶다고 가정해 보자.
이미 roomId 에 의존하는 effect 가 있으므로 거기에 분석 호출을 추가하고 싶을 수 있다.
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
하지만 나중에 이 effect 에 연결을 다시 설정해야 하는 다른 종속성을 추가한다고 가정해 보자.
이 effect 가 다시 동기화되면 의도하지 않은 동일한 방에 대해 logVisit(roomId) 도 호출한다.
방문 기록은 연결과는 다른 별개의 프로세스이다. 두 개의 개별 effect 로 작성하자.
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}
위 예시에선 한 effect 를 삭제해도 다른 effect 의 로직이 깨지지 않는다. 이는 서로 다른 것을 동기화하므로 분리하는 것이 합리적이라는 것을 알려준다. 반면 일관된 로직을 별도의 effect 로 분리하면 코드가 더 깔끔해 보일 순 있지만 유지 보수가 어려워진다.
➡️ 따라서 코드가 더 깔끔해 보이는지 여부가 아니라 프로세스가 동일하거나, 분리되어 있는지를 고려해야 한다.
5️⃣ 반응형 값에 '반응'하는 effect
위 예시의 effect 에서 두 개의 변수(serverUrl & roomId)를 읽지만 의존성 배열에 roomId 만 지정했다.
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
};
}, [roomId]);
}
serverUrl 이 의존성 배열에 포함될 필요가 없는 이유는 무엇일까? 리렌더링으로 인해 serverUrl 이 변경되지 않기 때문이다!
컴포넌트가 몇 번을 리렌더링하든 항상 동일하다. serverUrl 은 절대 변경되지 않으므로 의존성 배열에 포함하는 것은 의미가 없다.
의존성 배열의 요소는 시간이 지남에 따라 변경될 때만 무언가를 수행한다.
반면 roomId 는 리렌더링 시 달라질 수 있다.
컴포넌트 내부에 선언된 props, state 및 기타값은 렌더링 중에 계산되고, React 데이터 흐름에 참여하기 때문에 반응형 값이다.
serverUrl 이 state 변수라면 반응형일 것이다.(반응형 값은 의존성 배열에 포함되어야 한다.)
serverlUrl 을 의존성 배열에 포함하면 effect 가 변경된 후 다시 동기화되도록 할 수 있다.
function ChatRoom({ roomId }) { // Props는 시간이 지남에 따라 변화합니다.
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State는 시간이 지남에 따라 변화합니다.
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // effect는 Props와 state를 읽습니다.
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // 따라서 이 effect는 props와 state에 "의존"한다고 React에 알려줍니다.
// ...
}
6️⃣ 빈 의존성 배열이 있는 effect 의 의미
serverUrl 과 roomId 를 모두 컴포넌트 외부로 이동하면 어떻게 될까?
const serverUrl = 'https://localhost:1234';
const roomId = 'general';
function ChatRoom() {
useEffect(() => {
// ...
}, []); // ✅ 선언된 모든 종속성
// ...
}
이제 effect 의 코드는 어떤 반응형 값도 사용하지 않으므로 의존성 배열이 비어있다.
컴포넌트 관점에서 생각해 보면 빈 의존성 배열은 이 effect 가 컴포넌트가 마운트될 때만 채팅방에 연결되고 마운트 해제될 때만 연결이 끊어진다는 것을 의미한다.(React 는 로직 테스트를 위해 개발 단계에서 한 번 더 동기화한다.)
하지만 effcet 관점으로 보면 마운트 및 마운트 해제에 대해 전혀 생각할 필요가 없다. 중요한 것은 effect 가 동기화를 시작하고 중지하는 작업을 지정한 것이다. 현재는 반응형 종속성이 없다. 하지만 사용자가 roomId 또는 serverUrl 을 변경하려는 경우(반응형이 되는 경우) effect 의 코드는 변경되지 않는다. 종속성에 추가하기만 하면 된다.
7️⃣ 컴포넌트 내부의 모든 값 - props, state, 변수 는 반응형 : 모든 반응형 값은 리렌더링 시 변경될 수 있으니 종속 요소로 포함해야 한다.
props 와 state 만 반응형 값인 것은 아니다. 이들로부터 계산하는 값도 반응형이다. props 나 state 가 변경되면 컴포넌트가 리렌더링되고, 그로부터 계산된 값도 변경된다. 이 때문에 effect 에서 사용하는 컴포넌트 본문의 모든 변수는 effect 종속성 목록에 있어야 한다.
❗️ 즉, effect 는 컴포넌트 본문의 모든 값에 '반응'한다.
사용자가 드롭다운에서 채팅 서버를 선택할 수 있지만 설정에서 기본 서버를 구성할 수도 있다고 가정해 보자.
이미 settings state 를 context 에 넣어서 해당 context 에서 settings 를 읽었고, props 에서 선택한 서버와 기본 서버를 기준으로 serverUrl 을 계산한다.
function ChatRoom({ roomId, selectedServerUrl }) { // roomId는 반응형입니다.
const settings = useContext(SettingsContext); // settings는 반응형입니다.
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl는 반응형입니다.
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // effect는 roomId 와 serverUrl를 읽습니다.
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // 따라서 둘 중 하나가 변경되면 다시 동기화해야 합니다!
// ...
}
이 예시에서 serverUrl 은 prop 이나 state 변수가 아니다. 렌더링 중 계산하는 일반 변수이다.
하지만 렌더링 중 계산되므로 리렌더링 시 변경될 수 있다. 그러므로 반응형 값이다.
🧐 전역 or 변경할 수 있는 값은?
변경 가능한 값(전역 변수 포함)은 반응하지 않는다.
location.pathname 과 같은 변경 가능 값은 종속성이 될 수 없다. 이 값은 변경 가능하므로 React 렌더링 데이터 흐름 외부에서 언제든 변경할 수 있지만, 이 값을 변경해도 컴포넌트가 다시 렌더링되지는 않는다. 따라서 종속성에서 지정했더라도 React 는 effect 가 변경될 때 effect 를 다시 동기화할 지 알 수 없다. 또한 렌더링 도중(의존성을 계산할 때) 변경 가능한 데이터를 읽는 것은 렌더링의 순수성을 깨드리기에 React 의 규칙을 위반한다. 대신, useSyncExternalStore 를 사용해 외부 변경 가능 값을 읽고 구독해야 한다.
ref.current 와 같이 변경 가능한 값이나 이 값에서 읽은 것 역시 종속성이 될 수 없다. useRef 가 반환하는 ref 객체 자체는 종속성이 될 수있지만, current prop 은 의도적으로 변경할 수 있다. 이를 통해 리렌더링을 트리거하지 않고도 무언가를 추적할 수 있다. 하지만 변경해도 다시 렌더링이 트리거되지 않기 때문에 반응형 값이 아니며, React 는 이 값이 변경될 때 effect 를 다시 실행할 지 알지 못한다.
➕ 린터는 이러한 문제를 자동으로 확인한다.
8️⃣ React 는 모든 반응형 값을 종속성으로 지정했는지 확인한다.
린터가 React 에 대해 구성됐다면, effect 의 코드에서 사용되는 모든 반응형 값이 종속성에 지정되었는지 확인한다.
예시로 roomId 와 serverUrl 이 모두 반응형이기에 아래 코드는 린트 오류가 일어난다.
이것은 React 오류처럼 보일 수 있지만, 코드의 버그를 지적하는 것이다.
roomId 와 serverUrl 은 시간이 지남에 다라 변경될 수 있지만, 변경 시 effect 를 다시 동기화하는 것을 잊어버리고 있다.
(사용자가 UI 에서 다른 값을 선택해도 초기 roomId 와 serverUrl 에 연결된 상태로 유지된다.)
버그를 수정하려면 린터의 제안에 따라 effect 의 의존성 배열에 roomId 및 serverUrl 을 지정하자.
}, [serverUrl, roomId]); // ✅ 선언된 모든 종속성
❗️ 어떤 경우에선 컴포넌트 내부에 값이 선언되더라도 절대 변하지 않는다는 것을 React 가 알고 있다.
예시로, useState 에서 반환되는 set 함수와 useRef 에서 반환되는 ref 객체는 안정적이며, 다시 렌더링해도 변경되지 않도록 보장된다.
안정된 값은 반응하지 않으므로(변경되지 않으므로) 목록에서 생략할 수 있다.
9️⃣ 다시 동기화하지 않으려는 경우 어떻게 해야 할까?
위 예시에선 roomId 와 serverUrl 을 의존성 배열에 넣어 린트 오류를 해결했다.
이 방법 대신 이러한 값이 반응형 값이 아니라는 것, 즉 리렌더링으로 변경되지 않는 다는 것을 린터에 '증명'할 수도 있다.
예시로, roomId 와 serverUrl 이 렌더링에 의존하지 않고 항상 같은 값을 갖는다면 컴포넌트 외부로 옮길 수 있다.
const serverUrl = 'https://localhost:1234'; // serverUrl는 반응형이 아닙니다.
const roomId = 'general'; // roomId는 반응형이 아닙니다.
function ChatRoom() {
useEffect(() => {
// ...
}, []); // ✅ 선언된 모든 종속성
// ...
}
이제 종속성이 될 필요가 없다. 아니면 effect 내부로 이동시킬 수도 있다. 이는 렌더링 중 계산되지 않으므로 반응하지 않는다.
function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl는 반응형이 아닙니다.
const roomId = 'general'; // roomId는 반응형이 아닙니다.
// ...
}, []); // ✅ 선언된 모든 종속성
// ...
}
effect 는 반응형 코드 블록이다. 내부에서 읽은 값이 변경되면 다시 동기화된다. 상호작용당 한 번만 실행되는 이벤트 핸들러와 달리 effect 는 동기화가 필요할 때마다 실행된다.
종속성을 '선택'할 수 없다. 종속성엔 effect 에서 읽은 모든 반응형 값이 포함되어야 한다.(린터가 이를 강제한다.) 때때로 이에 따라 무한 루프와 같은 문제가 발생하거나 effect 가 너무 자주 재동기화될 수 있다. 하지만 그렇다고 린터를 억제하지 말고, 아래 방법을 시도해 보자.
- effect 가 독립적인 동기화 프로세스를 나타내는지 확인하라. effect 가 아무것도 동기화하지 않는다면 불필요한 것일 수 있다. 여러 개의 독립적인 것을 동기화한다면 분할하자.
- props 나 state 에 '반응'하지 않고, effect 를 다시 동기화하지 않고 최신 값을 읽으려면 effect 를 반응하는 부분(effect 에 유지할 것)과 반응하지 않는 부분(effect 이벤트라고 하는 것으로 추출할 수 있는 것)으로 분리하면 된다. 🔗
- 객체와 함수를 종속성으로 사용하지 말자. 렌더링 중 오브젝트와 함수를 생성한 다음 effect 에서 읽으면 렌더링할 때마다 오브젝트와 함수가 달라진다. 그러면 매번 effect 를 다시 동기화해야 한다. 🔗
2. Effect 에서 이벤트 분리하기 : 일부(불필요한) 값이 Effect 를 다시 발생시키는 것을 막는 방법 -Event 로 옮기자!
🔧 개발 중 : 이 섹션에선 아직 안정된 버전의 React 로 출시되지 않은 실험적인 API 에 대해 설명한다.
이벤트 핸들러는 같은 상호작용을 반복하는 경우에만 다시 실행되고, Effect 는 이벤트 핸들러와 달리 prop 이나 state 변수 등 읽은 값이 마지막 렌더링 때와 다르면 다시 동기화한다. 때로는 두 동작이 섞여 어떤 값에는 반응해 다시 실행되지만, 다른 값에는 그러지 않는 Effect 를 원할 때도 있다. 이를 effect 와 이벤트로 분리하자.
아래 예시에서 effect 내 모든 코드는 반응형이며, 읽은 반응형 값이 리렌더링으로 인해 변경되면 다시 실행된다.
(roomId 또는 theme 가 변경되면 채팅에 다시 연결된다.)
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'}
/>
</>
);
}
이것은 이상적이지 않다. roomId 가 변경된 경우에만 채팅에 다시 연결하고 싶다. theme 를 전환해도 채팅에 다시 연결되지 않아야 한다.
theme 를 읽는 코드를 Effect 에서 Effect Event 로 옮기자.
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>
}
Effect 이벤트 내부의 코드는 반응이 아니므로 theme 를 변경해도 더 이상 Effect 가 다시 연결하지 않는다.
이벤트 핸들러와 Effect 중에 선택하기
먼저 이벤트 핸들러와 Effect 의 차이점에 대해 간단히 알아보자. 채팅방 컴포넌트를 구현한다면 요구사항은 아래와 같다.
- 채팅방 컴포넌트는 선택된 채팅방에 자동으로 연결되어야 한다.
- '전송' 버튼을 클릭하면 채팅에 메세지를 전송해야 한다.
이 코드들이 이미 구현되었을 때, 이벤트 핸들러와 Effect 중 어디에 넣어야 할까?
해당 코드가 실행되어야 하는 이유를 고려해 보자.
1️⃣ 이벤트 핸들러는 특정 상호작용에 대한 응답으로 실행된다.
사용자 관점에서 메시지는 '전송' 버튼이 클릭되었기 때문에 전송되어야 한다. 그러므로 메시지를 전송하는 건 이벤트 핸들러가 되어야 한다.
이벤트 핸들러는 특정 상호작용을 처리하게 해주고, 사용자에게 버튼을 누를 때만 sendMessage(message) 가 실행된다고 알려준다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>전송</button>
</>
);
}
2️⃣ Effect 는 동기화가 필요할 때마다 실행된다.
채팅방 컴포넌트는 채팅방과의 연결을 유지해야 한다는 요구사항을 떠올려 보자. 이 코드를 실행하는 이유는 어떤 특정 상호작용 때문이 아니다. 사용자가 채팅방 화면으로 이동한 이유나 방법은 상관없다. 사용자가 현재 채팅방 화면을 보고 상호작용할 수 있으므로 컴포넌트는 선택된 채팅 서버에 계속 연결되어 있어야 한다. 채팅방 컴포넌트가 앱의 첫 화면이고 사용자가 아무런 상호작용을 하지 않은 경우라 해도 여전히 연결되어 있어야 한다. 그러므로 이 코드는 Effect 이다.
function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
이제 사용자가 수행하는 특정 상호작용에 상관없이 현재 선택된 채팅 서버와 항상 연결된 상태임을 확신할 수 있다.
사용자가 앱을 열기만 했든, 다른 방을 선택했든, 다른 화면으로 이동했다가 다시 돌아왔든, 컴포넌트가 현재 선택된 방과 동기화된 상태를 유지할 것이며 필요할 때마다 다시 연결할 것을 Effect 가 보장한다.
반응형 값과 반응형 로직
이벤트 핸들러는 버튼 클릭과 같이 항상 '수동으로' 트리거되지만, Effect 는 동기화 유지에 필요한 만큼 자주 실행/재실행되기 때문에 '자동으로' 트리거된다고 말할 수 있다. 이에 대해 더 자세히 알아보자.
컴포넌트 본문 내부에 선언된 props, state, 변수를 반응형 값이라 한다. 다음 예시에서 serverUrl 은 반응형 값이 아니지만 roomId 와 message 는 반응형 값이다. 이 반응형 값은 데이터 렌더링 과정에 관여한다.
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
}
이러한 반응형 값은 리렌더링으로 변경될 수 있다. 예시로 사용자가 message 를 편집하거나 드롭다운에서 다른 roomId 를 선택하는 경우가 있다. 이 때 이벤트 핸들러와 Effect 는 다르게 반응한다.
- 이벤트 핸들러 내부의 로직은 반응형이 아니다. 사용자가 같은 상호작용을 반복하지 않는 한 재실행되지 않는다. 이벤트 핸들러는 변화에 '반응'하지 않으면서 반응형 값을 읽을 수 있다.
- Effect 내부의 로직은 반응형이다. Effect 에서 반응형 값을 읽는 경우 그 값을 의존성으로 지정해야 한다. 그렇게 하면 리렌더링이 그 값을 바꾸는 경우 React 가 새로운 값으로 Effect 의 로직을 다시 실행한다.
이전 예시를 통해 이 차이에 대해 알아 보자.
1️⃣ 이벤트 핸들러 내부 로직은 반응형이 아니다.
// ...
sendMessage(message);
// ...
위 로직은 반응형일까, 아닐까?
사용자 관점에서 message 를 바꾸는 것은 메시지를 전송하고 싶다는 의미는 아니다. 사용자가 입력 중이라는 의미일 뿐이다.
즉 메시지를 전송하는 로직은 반응형이어선 안된다. 반응형 값이 변경되었단 이유만으로 로직이 재실행되어선 안된다. 그러므로 이 로직은 이벤트 핸들러에 속한다.
function handleSendClick() {
sendMessage(message);
}
이벤트 핸들러는 반응형이 아니므로 sendMessage(message) 는 사용자가 전송 버튼을 클릭할 때만 실행될 것이다.
2️⃣ Effect 내부의 로직은 반응형이다.
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...
위 로직은 반응형일까, 아닐까?
사용자 관점에서 roomId 를 바꾸는 것은 다른 방에 연결하고 싶다는 의미이다. 즉 방에 연결하기 위한 로직은 반응형이어야 한다.
우리는 이 코드가 반응형 값을 '따라가고' 그 값이 바뀌면 다시 실행되기를 원한다. 그러므로 이 로직은 Effect 에 속한다.
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);
Effect 는 반응형이므로 createConnection(serverUrl, roomId) 와 connection.connect() 는 구별되는 모든 roomId 값에 대해 실행될 것이다. Effect 는 채팅 연결과 현재 선택된 방의 동기화를 유지해 준다.
🔗 https://ko.react.dev/learn/lifecycle-of-reactive-effects
반응형 effects의 생명주기 – React
The library for web and native user interfaces
ko.react.dev