이벤트 핸들러는 같은 상호작용을 다시 할 때만 재실행돼요. 이벤트 핸들러와는 다르게 이펙트는 props이나 상태 변수와 같이 읽으려는 값이 이전에 렌더링을 할 때와 달라졌을 때 바뀌어요. 때때로 이 두 행동을 혼합하고 싶을 때가 있을 거예요. 특정 값에만 반응하여 재실행 되는 이펙트를 만들고 싶을 수도 있어요. 이 페이지에서는 어떻게 그런 이펙트를 만드는지 알려줄 거예요.
이 페이지에서는
- 이벤트 핸들러와 이펙트 사이에서 어떻게 선택할 것인지
- 이펙트가 왜 반응적이고 이벤트 핸들러가 왜 그렇지 않은지
- 이펙트의 코드가 반응적이지 않을 때는 무엇을 해야하는지
- 이펙트 이벤트가 무엇이고 이펙트에서 이들을 어떻게 분리해야하는지
- 최근 props와 상태를 이펙트 이벤트를 사용하여 이펙트에서 어떻게 읽는지
를 알아볼 거예요.
Choosing between event handlers and Effects | 이벤트 핸들러와 이펙트 사이에서 선택하기
먼저, 이벤트 핸들러와 이펙트의 차이점을 요약해볼게요.
채팅방 컴포넌트를 구현해야한다고 생각해볼게요. 요구사항은 아래와 같아요.
- 컴포넌트는 자동으로 선택된 채팅방에 연결돼요.
- "Send" 버튼을 누르면 채팅에 메시지를 보내요.
이미 이러한 요구사항을 만족하는 코드르 작성했지만 이 코드를 어디에 넣어야할지 모른다고 할게요. 이 때 이벤트 핸들러를 사용해야할까요, 아니면 이펙트를 사용해야할까요? 이 질문의 답이 필요할 때마다 왜 코드를 실행해야하는지를 고민해보세요.
Event handlers run in response to specific interactions | 이벤트 핸들러는 특정 상호작용에 반응하여 실행된다
사용자의 관점에서 메시지를 보내는 것은 특정 "Send" 버튼을 클릭했기 때문에 발생해요. 만약 메시지를 다른 이유 또는 다른 타이밍에 발송하면 사용자를 화나게 할 거예요. 메시지 전송을 이벤트 핸들러로 해야하는 이유가 바로 여기에 있어요. 이벤트 핸들러는 특정 상호작용을 다룰 수 있도록 만들어줘요.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>;
</>
);
}
이벤트 핸들러를 사용하면 sendMessage(message)
는 사용자가 버튼을 누를 때에만 실행된다는 것을 확신할 수 있어요.
Effects run whenever synchronization is needed | 이펙트는 동기화가 필요할 때마다 실행된다
컴포넌트가 채팅방과 연결 상태를 유지해야할 때를 회상해보세요. 이 코드는 어디에 삽입해야할까요?
이 코드를 실행하는 이유는 특정한 상호작용이 아니에요. 왜 또는 어떻게 사용자가 채팅방으로 이동했는지가 중요한게 아니에요. 이제는 사용자가 채팅방을 보고 있고 이 채팅방에서 상호작용해야한기 때문에 컴포넌트는 선택된 채팅 서버와 연결을 유지해야해요. 채팅방 컴포넌트가 앱의 초기화면이라고 하더라도, 그리고 사용자가 어떤 상호작용을 하지 않았다고 하더라도 연결은 여전히 유지되어야해요. 이 때문이 이펙트를 사용하면 안돼요.
function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
이 코드를 사용하면 사용자가 수행한 상호작용과 상관 없이 현재 선택된 채팅 서버와의 연결을 항상 활성화 시킬 수 있어요. 사용자가 앱을 열든, 다른 채팅방을 선택하든, 또는 또 다른 화면이나 뒤로 이동했든 이펙트는 컴포넌트가 현재 선택된 방과 동기화를 유지하고 필요할 때마다 다시 연결할 것임을 보장해요.
Reactive values and reactive logic | 반응값과 반응로직
직관적으로 이벤트 핸들러는 버튼을 누르는 것처럼 "수동적으로" 발생한다고 할 수 있어요. 반면 이펙트는 자동적이에요. 이펙트는 동기화를 유지할 필요가 있을 때마다 실행되고 재실행돼요.
이에 대해 더 정확하게 생각하는 방법이 있어요.
컴포넌트 바디에서 선언된 props, 상태 그리고 변수들은 반응값 이라고 불려요. 이 예시에서 serverUrl
은 반응값이 아니지만 roomId
와 message
는 반응값이에요. 렌더링 데이터 흐름에 이 값들은 참여해요.
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
}
이와 같은 반응값은 렌더링에 의해 바뀔 수 있어요. 예를 들어, 사용자는 message
를 수정하거나 드롭다운에서 다른 roomId
를 선택할 수 있어요. 이벤트 핸들러와 이펙트는 변화에 다르게 반응해요.
- 이벤트 핸들러의 로직은 반응적이지 않아요. 사용자가 같은 상호작용( 예: 클릭)을 다시 하지 않는다면 다시 실행되지 않아요. 이벤트 핸들러는 변화에 "반응"하지 않고 반응값을 읽을 수 있어요.
- 이펙트의 로직은 반응적이에요. 만약 이펙트가 반응 값을 읽는다면 의존성으로 지정해주어야해요. 그리고 나서 만약 그 값이 리렌더링이 되면서 변경된다면 리액트는 이펙트의 로직을 새로운 값으로 재실행해요.
이 차이를 설명하는 이전의 예시를 다시 화인해보세요.
Logic inside event handlers is not reactive | 이벤트 핸들러 내부의 로직은 반응적이지 않다
이 코드를 보세요. 이 로직이 반응적이나요, 아닌가요?
// ...
sendMessage(message);
// ...
사용자의 관점에서 message
가 변경되는 것은 메시지를 보내고 싶다는 것을 의미하지 않아요. 사용자가 타이핑을 친다는 것을 의미하죠. 즉, 메시지를 전송하는 로직은 반응하면 안돼요. 단지 반응값이 바뀌었다고 재실행 되어서는 안돼요. 그렇기 때문에 이 코드는 이벤트 핸들러에 들어가 있어요.
function handleSendClick() {
sendMessage(message);
}
이벤트 핸들러는 반응적이지 않기 때문에 sendMessage(messgae)
는 사용자가 전송 버튼을 클릭할 때만 반응해요.
Logic inside Effects is reactive | 이펙트 내부의 로직은 반응적이다
이제 다시 아래의 코드를 볼게요.
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...
사용자의 관점에서 roomId
의 변화는 다른 방과 연결하고 싶다는 것을 의미하지 않아요. 즉, 채팅방으로 연결하는 로직은 반응적이면 안돼요. 이 코드를 반응값으로 유지하고 값이 달라진다면 다시 실행할 거예요. 그렇기 때문에 코드는 이펙트에 들어가 있어요.
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);
이펙트는 반응적이기 때문에 createConnection(serverUrl, roomId)
와 connection.connect()
는 roomId
의 모든 값으로 실행될 거예요. 이펙트는 현재 채팅방과 채팅 연결을 동기화한 상태로 유지해줘요.
Extracting non-reactive logic out of Effects | 반응적이지 않은 로직을 이펙트에서 추출하기
반응로직와 반응적이지 않은 로직을 혼합해서 사용하고 싶을 때 이는 조금 더 복잡해져요.
예를 들어, 사용자가 채팅방과 연결할 때 알림을 보여준다고 해볼게요. 현재 테마(다크 또는 라이트)를 props로 읽어서 알맞은 색상으로 알림창을 보여줄 수 있어요.
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
// ...
그러나 theme
은 반응 값이고(리렌더링의 결과에 따라 달라질 수 있어요) 이펙트에서 읽는 모든 반응값은 의존성으로 선언되어야해요. 이제 theme
을 이펙트의 의존성으로 지정해볼게요.
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ 모든 의존성이 선언되었어요.
// ...
이 예시를 만져보고 이러한 사용자 경험의 문제점을 찾아내보세요.
roomId
가 변할 때, 채팅은 예상한대로 재연결돼요. 하지만 theme
또한 의존성이기 때문에 다크 테마와 라이트 테마로 변경할 때에도 재연결돼요. 이건 좋지 않아요!
이 말은 즉, 설령 (반응적인) 이펙트 안에 있더라도 아래의 코드도 반응로직이 아니라는 거예요.
// ...
showNotification('Connected!', theme);
// ...
반응적인 이펙트에서 이 반응적이지 않은 로직을 분리할 방법이 필요해요.
Declaring an Effect Event | 이펙트 이벤트 선언하기
개발 중
이 섹션은 리액트의 안정적인 버전에서는 아직 공개되지 않은 실험적인 API를 설명해요.
useEffectEvent
라는 특별한 훅을 사용하여 반응적이지 않은 로직을 이펙트 외부로 추출할 거예요.
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
// ...
여기서 onConncected
는 이펙트 이벤트라고 불려요. 하지만 이벤트 핸들러와 더 비슷하게 동작해요. 그 안의 로직은 반응적이지 않고 항상 props와 상태의 최신값을 "봐요."
이제 onConnected
이펙트 이펙트를 이펙트 안에서 호출할 수 있어요.
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]); // ✅ 모든 의존성이 선언되었어요.
// ...
이렇게 하면 문제가 해결돼요. 이펙트의 의존성 배열에서 onConnected
를 제거해야만 한다는 사실을 기억하세요. 이펙트 이벤트는 반응적이지 않고 의존성에서 생략되어야해요.
새로운 행동이 원하는 대로 잘 동작하는지 확인해보세요.
이펙트 이벤트를 이벤트 핸들러와 굉장히 비슷하다고 생각할 수 있어요. 가장 큰 차이점은 이벤트 핸들러는 사용자의 상호작용에 반응하여 실행되지만 이펙트 이벤트는 이펙트에서 발생시킨다는 점이에요. 이펙트 이벤트는 이펙트의 반응성과 반응적이면 안되는 코드 사이의 "연결고리를 깨뜨려줘요."
Reading latest props and state with Effect Events | 이펙트 이벤트로 최신 props와 상태 읽기
개발 중
이 섹션은 리액트의 안정적인 버전에서는 아직 공개되지 않은 실험적인 API를 설명해요.
이펙트 이벤트는 의존성 린터를 막고 사용하는 여러 패턴을 고쳐줘요.
예를 들어 페이지 방문 로그를 저장하는 이펙트가 있어요.
function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}
나중에 사이트에 여러 라우트를 추가할 수도 있어요. 이제 여러분의 Page
컴포넌트는 현재 경로를 url
prop으로 받아요. url
을 logVisit
호출에 전달하고 싶지만 의존성 린터는 여기에 경고문을 달아요.
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 useEffect 훅은 잃어버린 의존성 'url'이 있어요
// ...
}
코드가 하고 싶은 일에 대해 생각해보세요. 각 URL은 서로 다른 페이지를 표현하고 있기 때문에 다른 URL에 대한 각긱 다른 방문을 분리해서 로그하고 싶어요. 다른 말로 하면 이 logVisit
호출은 url
에 대하여 반응적이어야해요. 그렇기 때문에 이 경우에는 의존성 린터를 따라가서 의존성에 url
을 넣는 것이 더욱 합리적이에요.
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ 모든 의존성이 선언되었어요.
// ...
}
이제 모든 페이지를 방문할 때 장바구니의 아이템들을 포함하고 싶다고 해볼게요.
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 useEffect 훅은 잃어버린 의존성 'numberOfItems'이 있어요R
// ...
}
numberOfItems
를 이펙트 안에서 사용했지만 린터는 이 변수를 의존성에 넣기를 요청했어요. 그러나 logVist
호출이 numberOfItems
에는 반응적이지 않기를 바래요. 만약 사용자가 무언가를 장바구니에 넣었고 numberOfItems
가 변했다고 해도 이것이 사용자가 페이지를 다시 방문했다는 것을 의미하지는 않아요. 즉, 페이지를 방문하는 것은 어떤 의미로는 "이벤트"에요. 특정한 순간에만 발생하니까요.
코드를 두 파트로 분리해볼게요.
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ 모든 의존성이 선언되었어요
// ...
}
여기서 onVisit
는 이펙트 이벤트에요. 그 안의 코드는 반응적이지 않아요. 그렇기 때문에 변화에 맞춰 주변 코드를 재실행시킬 것이라는 걱정 없이 numberOfItems
(또는 다른 반응값)를 사용할 수 있어요.
반면, 이펙트 자체는 반응적으로 남아있어요. 이펙트 내부의 코드는 url
prop을 사용하기 때문에 이펙트는 다른 url
을 가지는 모든 리렌더링 후에 재실행 돼요. 이러한 동작은 onVisit
이펙트 이벤트를 차례로 호출할 거예요.
결론적으로 logVisit
를 url
이 바뀔 때마다 호출하고 항상 최신 numberOfItems
를 읽을 거예요. 그러나 만약 numberOfItem
자체가 바뀐다면 어떤 코드도 재실행시키지 않아요.
노트
만약onVisit()
를 인자 없이 호출하거나 그 안에서url
을 읽는다면 어떻게 되는지 궁금할 거예요.
const onVisit = useEffectEvent(() => { logVisit(url, numberOfItems); }); useEffect(() => { onVisit(); }, [url]);
이 코드 또한 동작하지만 이url
을 이펙트 이벤트에 명시적으로 전달하는 것이 더 좋아요.url
을 이펙트 이벤트에 인자로 전달한다면 다른url
로 페이지를 방문하는 것은 사용자의 관점에서는 별개의 "이벤트"를 만들었다고 말할 수 있어요.visitedUrl
은 발생한 "이벤트"의 일부에요.
이펙트 이벤트가 명시적으로const onVisit = useEffectEvent(visitedUrl => { logVisit(visitedUrl, numberOfItems); }); useEffect(() => { onVisit(url); }, [url]);
visitedUrl
을 "요청"했기 때문에 실수로 이펙트의 의존성에서url
을 제거할 수 없어요. 만약url
의존성을 제거한다면 (다른 페이지 방문도 하나로 카운팅되도록 만들수도 있는 행동이에요) 린터는 이에 대해 경고할 거예요.onVisit
가url
에 반응적이기를 바라기 때문에url
을 내부에서 읽는 대신 (이렇게 하면 반응적이지 않아요) 이펙트에서 전달해주세요.
비동기적인 로직이 이펙트 안에 있다면 이는 특히 더 중요해져요.
const onVisit = useEffectEvent(visitedUrl => { logVisit(visitedUrl, numberOfItems); }); useEffect(() => { setTimeout(() => { onVisit(url); }, 5000); // 방문 로그 지연 시키기 }, [url]);
onVisit
내부의url
은 최신의url
에 해당하지만 (이미 변경된 값)visitedUrl
은 기존에 이펙트가 실행되도록 만든 (그리고 이onVisit
에서 호출된)url
에 해당해요.
이 대신 의존성 린트를 막는것도 괜찮나요?
더 알아보기기존의 코드에서 때때로 아래와 같이 린트 규칙을 꺼버리는 경우를 볼 수도 있어요.
function Page({ url }) { const { items } = useContext(ShoppingCartContext); const numberOfItems = items.length; useEffect(() => { logVisit(url, numberOfItems); // 🔴 아래와 같이 린터를 억제하는 것을 피하세요 // eslint-disable-next-line react-hooks/exhaustive-deps }, [url]); // ... }
useEffectEvent
가 리액트에 정식으로 추가된다면 린터를 절대 억제하지 마세요.
린터 규칙을 해제할 때 발생하는 첫 번째 문제는 이펙트가 코드에서 사용되는 새로운 반응적인 의존성에 반응할 필요가 있을 때에도 더 이상 경고하지 않을 것이라는 점이에요. 이전의 예시에서url
을 의존성 배열로 추가했어요. 리액트가 그렇게 하라고 리마인드 해줬기 때문이에요. 만약 린터를 비활성화한다면 이후에 발생하는 이펙트의 수정에서 이러한 리마인더를 받지 못할 거예요. 이는 곧 버그로 이어질 수 있어요.
린터를 해제해서 발생한 혼란스러운 버그의 예시에요. 이 예시에서handleMove
함수는 점이 커서를 따라가야하는지를 결정하기 위하여 상태 변수인canMove
의 현재값을 읽어와야해요. 하지만handleMove
에서canMove
는 항상true
에요.
이유를 찾을 수 있나요?
이 코드의 문제를 의존성 린터를 해제했다는 점이에요. 만약 해제하지 않았다면 이 이펙트는
handleMove
함수에 의존해야한다는 것을 볼 수 있어요.handleMove
는 컴포넌트 바디에서 선언되었고 이는 이 코드가 반응적이라는 거예요. 모든 반응 값은 의존성으로 지정되어야하고, 만약 지정되지 않았다면 시간이 지나더라도 예전의 값을 가지고 있을 가능성이 있어요!
기존 코드의 작성자는 이펙트가 어떤 반응 값에도 의존하지 않는다(
[]
)고 리액트에게 "거짓말을 하고 있어요." 리액트가canMove
(와handleMove
)가 바뀐 이후에도 이펙트를 재동기화시키지 않는 이유에요. 리액트는 이펙트를 재동기화 시키지 않기 때문에 리스너에 붙어있는handleMove
는 초기 렌더링에서 생성된handleMove
함수에요. 초기 렌더링 동안canMove
는true
이고 이는 초기 렌더링의handleMove
가 영원히 그 값만을 보는 이유에요.
만약 린터를 막지 않았다면 오래된 값으로 생기는 문제를 보지 않았을 거예요.
useEffectEvent
를 사용하면 린터를 속일 필요가 없고 코드는 원하는대로 작동할 거예요.
useEffectEvent
가 알맞은 해결책을 항상 제공한다는 것을 의미하지는 않아요. 반응적이고 싶지 않은 코드에만 적용해야해요. 위의 샌드박드에서 이펙트의 코드는canMove
에 반응하고 싶지 않았아요. 그래서 이펙트 이벤트로 해당 코드를 추출했어요.
이펙트 의존성 제거하기문서에서 린터를 막는 대신 사용할 수 있는 다른 방법들을 읽어볼 수 있어요.
Limitations of Effect Events | 이펙트 이벤트의 한계
개발 중
이 섹션은 리액트의 안정적인 버전에서는 아직 공개되지 않은 실험적인 API를 설명해요.
이펙트 이벤트는 제한적으로 사용해야해요.
- 이펙트 내부에서만 호출하세요.
- 다른 컴포넌트나 훅으로 이들을 전달하지 마세요.
그 예로 아래와 같이 이펙트 이벤트를 선언하고 전달하지 마세요.
function Timer() {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
setCount(count + 1);
});
useTimer(onTick, 1000); // 🔴 이펙트 이벤트를 전달하는 것은 피하세요
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // 의존성에 "callback"을 지정해줘야해요.
}
대신, 이펙트 이벤트는 이들을 사용하는 이펙트 바로 옆에서 선언하세요.
function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});
useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ 이펙트 안에서 지역적으로 호출하세요.
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // "onTick"(이펙트 이벤트)을 의존성으로 지정할 필요가 없어요.
}
이펙트 이벤트는 이펙트 코드의 반응적이지 않은 "조각"이에요. 이펙트 이벤트는 이들을 사용하는 이펙트 옆에 있어야해요.
Recap | 요약
- 이벤트 핸들러는 특정한 상호작용에 반응하여 실행돼요.
- 이펙트는 동기화가 필요할 때마다 실행돼요.
- 이벤트 핸들러 내부의 로직은 반응적이지 않아요.
- 이펙트의 로직은 반응적이에요.
- 이펙트의 반응적이지 않은 로직을 이펙트 이벤트로 옮길 수 있어요.
- 이펙트 이벤트는 이벤트 안에서만 호출하세요.
- 이펙트 이벤트를 다른 컴포넌트와 훅을 전달하지 마세요.
Challenges | 도전 과제
1. 업데이트되지 않는 변수 고치기
이 Timer
컴포넌트는 매 초마다 증가하는 상태 변수, count
를 갖고 있어요. 증가폭은 상태 변수 increment
에 저장돼요. 플러스와 마이너스 버튼으로 increment
변수를 조작할 수 있어요.
그러나 플러스 버튼을 몇번이나 눌렀든 카운터는 여전히 매 초마다 1씩 증가해요. 이 코드의 문제가 무엇일까요? 왜 increment
는 항상 이펙트의 코드에서 1
로 유지될까요? 실수를 찾아내서 고쳐보세요.
Hint
이 코드를 고치려면 규칙을 따르는 것만으로도 충분해요.
Solution
보통 그랬던 것처럼 이펙트에서 버그를 찾을 때 린터를 해제한 부분부터 살펴보세요.
만약 이 주석을 제거하면 리액트는 이 이펙트의 코드가 increment
에 의존하고 있지만 여러분은 이 이펙트가 어떤 반응값에도 의존하고 있지 않다고([]
) 주장하여 리액트를 속이고 있다는 것을 알려줘요. increment
를 의존성 배열에 추가하세요.
이제 increment
가 바뀌면 리액트는 이펙트와 재동기화하고 이는 인터벌을 재시작하게 만들어요.
2. 멈춰있는 카운터 수정하기
이 Timer
컴포넌트는 매 초마다 증가하는 상태 변수, count
를 갖고 있어요. 증가폭은 상태 변수 increment
에 저장돼요. 플러스와 마이너스 버튼으로 increment
변수를 조작할 수 있어요. 예를 들어 플러스 버튼을 9번 눌러보세요. 그러면 count
는 매초 1이 아니라 10씩 증가할 거예요.
이 유저 인터페이스에서는 작은 이슈가 있어요. 만약 플러스나 마이너스 버튼을 1초에 한 번 누르는 속도보다 더욱 빠르게 누른다면 타이머 자체는 멈춘 것처럼 보일 거예요. 이 두 버튼을 마지막으로 클릭한 이후에 몇 초가 지나서야 다시 카운팅이 재개돼요. 이렇게 작동하는 이유를 찾고 이슈를 해결하여 타이머가 방해 없이 매 초마다 증가하도록 만드세요.
Hint
타이머를 설정하는 이펙트가 increment
값에 반응하는 것처럼 보여요. setCount
를 호출하기 위하여 현재 increment
값을 사용하는 줄이 정말로 반응적이어야만 할까요?
Solution
이 이슈는 이펙트 내부의 코드가 increment
를 사용하고 있어서 발생해요. 이 변수가 이펙트의 의존성이기 때문에 increment
의 모든 변화는 이펙트의 재동기화를 발생시켜요. 그리고 이는 인터벌이 정리되도록 만들죠. 만약 이 부분이 실행되기 전에 인터벌을 매순간 정리한다면 타이머가 정지된 것처럼 보일 거예요.
이 문제를 해결하기 위하여 이 부분을 이펙트에서 이펙트 이벤트인 onTick
으로 추출하세요.
onTick
은 이펙트 이벤트이기 때문에 내부 코드는 반응적이지 않아요. increment
의 변화는 어떤 이펙트도 발생시키지 않아요.
3. 조절할 수 없는 지연 고치기
이 예시에서 인터벌의 지연을 커스터마이징 할 수 있어요. 두 개의 버튼으로 업데이트 되는 상태 변수 delay
에 저장돼요. 하지만 delay
가 1000 밀리초(1초)가 될 때까지 "puls 100 ms" 버튼을 누른다 하더라도 타이머는 여전히 굉장히 빠르게 증가해요. (100 ms 마다) delay
를 바꾸는 행동은 무시되는 것 같아요. 버그를 찾고 고쳐보세요.
Hint
이펙트 이벤트 내부의 코드는 반응적이지 않아요. setInterval
호출이 재실행되어야하는 경우가 있나요?
Solution
위 예시의 문제점은 어떤 코드를 실제로 실행해야하는지를 고민하지 않고 onMount
를 호출하는 이펙트 이벤트로 추출되었다는 점이에요. 코드의 일부를 반응적이지 않도록 만들고 싶을 때와 같은 특정한 이유가 있을 때에만 코드를 이펙트 이벤트로 추출해야해요. 그러나 setInterval
호출은 delay
에 대해서만 반응적이어야만 해요. 만약 delay
가 바뀌면 스크래치에서 인터벌을 설정해야해요. 이 코드를 고치려면 모든 반응적인 코드를 이펙트 안으로 옮기세요.
일반적으로, 코드의 목적이 아니라 타이밍에 초점을 맞춘 onMount
같은 함수는 의심해봐야해요. 처음에는 조금 더 "자세하게" 느껴질 수는 있으나 의도를 모호하게 만들어요. 경험 법칙에 의하여 이펙트 이벤트는 사용자의 관점으로 동작하는 것들이에요. onMessage
, onTick
, onVisit
또는 onConnected
는 이펙트 이벤트의 좋은 이름들이에요. 이들 내부 코드는 반응적일 필요가 없어요. 반면 onMount
, onUpdate
또는 onAfterRender
는 너무 일반적이어서 내부에 반응적이어햐만 하는 코드를 실수로 넣기 쉬워요. 어떤 코드가 실행될 때가 아니라 사용자가 어떤 일이 일어났다고 생각하는 것을 이펙트 이벤트로 명명해야하는 이유에요.
4. 지연된 알림 고치기
채팅 룸에 참여할 때, 이 컴포넌트는 알림을 보여줘요. 그러나 즉시 알림창이 뜨는 건 아니에요. 대신, 알림창은 인위적으로 2초간 지연되어있어 사용자는 UI를 둘러볼 기회가 생겨요.
거의 잘 동작하지만 버그가 있어요. 드롭다운을 "general"에서 "travel"로 변경하고 "music"으로 굉장히 빠르게 변경해보세요. 만약 충분히 빨랐다면 두 개의 알림창을 보겠지만(기대한 것처럼요!) 둘 다 "Welcome to music"이라고 적혀있을 거예요.
이를 수정하여 "general"에서 "travel"로 바꾸고 그 직후에 재빠르게 "music"으로 바꿀 때, 첫 번째 알림창은 "Welcome to travel"으로, 두 번째 알림창은 "Welcome to music"으로 뜨도록 만드세요. (추가적인 도전과제로는 이미 알맞은 방을 보여주는 알림창을 만들었으면 코드를 변경하여 두 번째 알림창만 보이도록 만들어보세요.)
Hint
이펙트는 어떤 방과 연결되어있는지를 알고 있어요. 이펙트 이벤트로 전달해야하는 어떤 정보가 있나요?
Solution
이펙트 이벤트 안에서 roomId
는 이펙트 이벤트가 호출된 시점의 값이에요.
이펙트 이벤트는 2초의 지연 후에 호출돼요. 만약 빠르게 travel 방에서 music 방으로 변경되면 travel 방의 알림창이 보여지는 시점에 roomId
는 이미 "music"
이 돼요. 이 떄문이 두 알림창이 모두 "Welcome to music"이라고 나오는 거예요.
이 문제를 해결하려면 최근 roomId
를 읽는 대신 roomId
를 이펙트 이벤트의 매개변수로 넣으세요. 아래의 connectedRoomId
처럼요. 그리고나서 이펙트에서 onConnected(roomId)
를 호출하여 roomId
를 전달하세요.
"travel"
로 설정된 roomId
를 갖고 있는 (그래서 "travel"
방과 연결되어 있는) 이펙트는 "travel"
의 알림창을 보여줄 거예요. "music"
으로 설정된 roomId
를 갖고 있는 (그래서 "music"
방과 연결되어 있는) 이펙트는 "music"
의 알림창을 보여줄 거예요. 즉, theme
이 항상 최신 값을 사용하는 반면 connectedRoomId
는 (반응적인) 이펙트에서 왔어요.
추가 과제를 해결하려면 알림창의 timeout ID를 보관하고 이펙트의 클린업 함수에서 이를 정리하세요.
이렇게 하면 이미 스케줄링 되어 있는 (하지만 아직 보여지지 않은) 알림창은 방이 변경되면 취소될 거예요.