안녕하세요! YB 이윤지 입니다.
벌써 7주차고... 마지막 주차를 앞두고 있네요... 시간이 정말 빠르네요..
이번 주차는 Effect 에 대해서 톺아보기! 하려고 합니다.
모르기도 몰랐고... 범위도 꽤 많았는데 도저히 하나를 고를 수가 없었습니다...하하.
공부하는 겸해서 쫌쫌따리 썼으니까 챕터 별로, 입맛대로 골라 보시는 것도 좋겠네요!
이번 아티클은 공식문서...만으로도 이해가 안 가는 부분이 꽤나 있어서 이런 저런 글들을 참고하고 썼습니다!
참고한 부분은 밑에 링크 달아 둘게요~ 한 번씩 읽어보시면 좋을 것 같아요!
그런데 살짝 스압 주의..
레스기.
What is Effect?
앞서 UI 표현하기, 상호작용 더하기에서 공부했던 렌더링코드, 이벤트 핸들러 다들 기억하고 계시나요?
둘 다 컴포넌트 내부의 2가지 로직이죠!
렌더링 코드 | 이벤트 핸들러 | |
역할 | UI 를 화면에 그리는 역할 | 사용자 상호작용에 다른 동작을 처리 |
위치 | 컴포넌트의 최상단 | 컴포넌트 내부의 중첩 함수로 존재 |
입력 | `props` 와 `state` | 사용자 이벤트 (ex. 클릭, 입력, 포커스 ...등) |
출력 | JSX 반환 (화면에 그려질 UI 구조) | side Effect( 상태 업데이트, API 호출, 페이지 이동 등) |
순수성 | 순수 함수 (동일한 입력 -> 동일한 출력) | 순수하지 않음 (외부 상태 변경, 부수 효과 발생) |
역할 예시 | `props.title` 을 받아 화면에 제목을 렌더링 | 버튼 클릭 시 `onClick` 이벤트로 상태 업데이트 |
기능 예시 | return <h1>{props.title}</h1>; | function handleClick() { setState(true);} |
side Effect | X | O (상태 변경, 네트워크 요청 등) |
수행 동작 | 단순 계산 (UI 그리기) | 사용자 입력에 따른 행동 수행 ( 상호작용 처리 ) |
하지만 가끔은 위 두 가지 로직으로 충분하지 않습니다.
화면에 보일 때마다 채팅 서버에 접속해야 하는 ChatRoom 컴포넌트를 생각하면, 서버에 접속하는 것은 순수한 계산이 아니고 부수 효과를 발생시키기 때문에 렌더링 중에는 할 수가 없습니다.
하지만? 클릭 한 번으로 `ChatRoom` 이 표시되는 특정 이벤트도 없습니다.
그렇다면 Effect는?
Effect | |
역할 | 컴포넌트를 외부 시스템 (네트워크, 타이머, 로컬 스토리지 등) 과 동기화 |
위치 | 컴포넌트 내부에서 `useEffect` 훅을 사용하여 정의 |
입력 | 컴포넌트의 렌더링, 상태 변화, 또는 `props` 변경 |
출력 | 외부 시스템과의 동기화 (API 호출, 타이머 설정, DOM 조작 등) |
순수성 | 순수하지 않음 (외부 상태에 의존하고 변경함) |
역할 예시 | 컴포넌트가 렌더링될 때 서버에 데이터 요청 |
기능 예시 | useEffect(() => { fetchData();}, []); |
side Effect | API 호출, 타이머 설정, 로컬 스토리지 접근, 외부 리소스 관리 |
수행 동작 | 컴포넌트 렌더링 후 외부 시스템과 동기화 작업 수행 |
그러니까?
사용자가 채팅에서 메시지 보내는거 -> 이벤트 (사용자가 특정 버튼을 클릭함에 따라 직접적으로 발생)
서버연결 설정은? -> Effect ( 얘는 컴포넌트의 표시를 주관하는 어떤 상호 작용과도 상관없이 발생해야 하기 때문에)
그리고 Effect는 커밋이 끝난 후에 화면 업데이트가 이루어지고 나서 실행됩니다.
한 마디로 useEffect 훅은
1. 컴포넌트가 렌더링 될 때 특정 작업을 실행할 수 있도록 하는 Hook
2. 리액트의 useEffect 훅을 사용하면 함수 컴포넌트에서도 side Effect를 사용할 수 있다
- 클래스형 컴포넌트에서는 생명주기 메소드를 사용할 수 있었는데, 이를 함수형 컴포넌트에서도 사용할 수 있다
-> 즉? 라이프 사이클 훅을 대체할 수 있게 되었다는 뜻!
(componentDidMount, componentDidUpdate, componentWillUnmount)
요롱거요! 이해가 안된다 싶으면 밑에 펼쳐보기~
이전에 공부한 내용에 따르면 리액트에서는 컴포넌트를 크게 두 가지 형식으로 선언할 수 있죠?
Class 형과 Function 형으로요.
Class 형에서는
import {Component} from 'react';
export default class ClassComponent extends Component{
//Lifecycle Methods
//컴포넌트 안에 변화가 있을 때, 로딩될때를 리액트에서는 mount 라고 하죠.
//"장착했다"
//update 됐을때, 장착한게 해체될 때
//function 이나 코드를 실행할 수 있는 기능
componentDidMount(){
//컴포넌트가 처음으로 장착 됐을 때 실행해라
}
componentDidUpdate(){
//컴포넌트 안에 데이터가 업데이트 됐으면 실행해라
}
componentWillUnmount(){
//컴포넌트가 해체될 때, 바로 전에 실행되는 코드
}
render(){
return{
}
}
}
이런 식으로 LifeCycle 이 돌아요. 이건 Class Component 안에만 있어요.
하지만 Function Component 를 사용할 때는 LifeCycle Method 가 없기 때문에
useEffect 훅을 import 해 와서 LifeCycle 처럼 쓰는 거죠!
-> 그럼 그냥 Class Component 를 쓰면 되잖아!
Nope! 우리 벌써 1주차때 공부했죠?
https://wave-web.tistory.com/35
[week 1]- function Component ? class Component?
안녕하세요! YB 이윤지라고 합니다.리액트 첫 스터디인 만큼 무엇에 관한 주제로 아티클과 발표를 다뤄볼까 고민도 많이 했는데요!역시 리액트하면 떠오르는 건 컴포넌트 재사용성! 이 가장 많
wave-web.tistory.com
Effect 를 실행하는 법
1. Effect 선언하기
import { useEffect } from 'react';
2. 컴포넌트의 최상위 레벨에서 호출하고 Effect 내부에 코드 넣기
function MyComponent() {
useEffect(() => {
// 이곳의 코드는 *모든* 렌더링 후에 실행됩니다
});
return <div />;
}
컴포넌트가 렌더링 될 때마다 React는 화면을 업데이트 한 다음 useEffect 내부의 코드를 실행합니다.
그러니까 useEffect는 화면에 렌더링이 반영될 때까지 코드 실행을 "지연" 시키는 거죠.
리액트 공식문서에 있던 <videoPlayer> 을 사용한 예를 들었는데요.
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
return <video ref={ref} src={src} loop playsInline />;
}
여기서 문제는 <video> 태그에는 isPlaying prop 이 없다는 것이고, 이를 제어하는 유일한 방법은 DOM 요소에다가 수동으로 play() 및 pause() 메서드를 호출하는 것입니다.
이러려면 <video> DOM 노드의 ref를 가져와야 하는데
play() 또는 pause() 를 렌더링 중에 호출하려고 시도할 수 있지만, 리액트는 뭐다?
렌더링이 JSX의 순수한 계산 이어야 한다!!
그래서 위의 코드가 올바르지 않은 이유는 렌더링 도중에 DOM 노드를 조작하려고 시도하기 때문에
해결책은 부수 효과를 렌더링 연산에서 분리하기 위해서 useEffect로 감싸는 것입니다.
DOM 업데이트를 Effect로 감싸면 React 가 화면을 업데이트 한 다음에 Effect 가 실행됩니다.
이거 근데 예시가 렌더링이 잘 안돼서 밑에서부터는 제가 실습한 코드로 하겠습니다..
Effect 의 의존성 지정하기
Effect 모든 렌더링 이후에 실행되기 때문에
1. 때때로 느릴 수 있고
2. 때때로 잘못될 수 있습니다.
모든 키 입력마다 채팅 서버에 다시 연결하길 원하지 않을 것이고, 모든 키 입력마다 애니메이션을 트리거하지 하고 싶지 않을 수도 있겠죠?
애니메이션은 컴포넌트가 처음 나타날 때만 한 번! 실행되어야 합니다.
이걸 확인하기 위해서
useEffect(() => {
console.log("component is loaded");
});
이렇게 콘솔을 찍어봤는데요.
아 맞다. 제가 만든 예시는 버튼을 누르면 색이 바뀌는 거예요 ㅎㅎ...
버튼을 누를 때마다 콘솔 옆의 숫자가 올라가는 것을 볼 수 있었어요.
거슬리죠? (거슬리잖아요)
이렇게 불필요하게 다시 실행하지 않도록 하려면 useEffect의 두 번째 인자로 의존성 배열을 지정하면 됩니다!
useEffect(() => {
console.log("component is loaded");
}, []); // 빈 배열로 useEffect가 컴포넌트가 마운트될 때 한 번만 실행
요로코롬
공식문서 예시에서는 의존성 배열에 지시한 종속성이 Effect 내부의 코드를 기반으로 React가 기대하는 것과 일치하지 않으면 린트 에러가 발생한다고 합니다. 이를 통해 코드 내의 많은 버그를 잡을 수 있고 코드가 다시 실행되길 원치 않는 경우, Effect 내부를 수행하여 그 종속성이 "필요"하지 않게 만드세요!
리액트 공식문서에서는 의존성 배열이 있고, 제 코드에서는 빈 '[]' 의존성 배열이 있잖아요?
이 두 개의 동작이 다르다고 합니다!
useEffect(() => {
// 모든 렌더링 후에 실행됩니다
});
useEffect(() => {
// 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)
}, []);
useEffect(() => {
// 마운트될 때 실행되며, *또한* 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다
}, [a, b]);
한 마디로 제 코드는 컴포넌트가 처음 마운트 될 때만 사용하고, 이후 어떠한 업데이트에서도 실행되지 않는 반면,
리액트 공식문서의 코드에서는 처음 마운트 될 때 실행되고, 이후 isPlaying 이 변경될 때마다 실행된다는 거죠.
의존성 배열 | 실행 시점 | 그래서 뭐? |
없음 | 모든 렌더링 후에 실행 | 상태 변경, 리렌더링 때마다 계속 실행됨 |
[] | 마운트 될 때만 실행 | 한 번만 실행되고 이후에는 재실행 되지 않음. ( ex. 초기화나 API 호출 시 사용) |
[a,b] | 마운트 및 a 또는 b가 변경될 때 실행 | 특정 상태값이 변경될 때만 실행됨 |
제가 표를 좋아해요...
필요하다면 클린업을 추가하세요!
클린업?
clean-up 함수는 useEffect 를 사용해서 side effect 를 만들게 되면 정리가 필요한 상황이 오게 되는데,
이때 useEffect 콜백함수 안에서 정리 함수를 리턴하면 됩니다. 즉! side effect를 정리하는 Clean up 함수가 필요한거죠.
밑에 참고한 블로그가 있으니 한 번 읽어보셔요!
https://jjang-j.tistory.com/82
[React] useEffect 클린업(Clean Up) 함수로 메모리 누수 방지
시작하며... memo, useMemo, useCallback 사용해서 렌더링 최초화 하기!!시작하기 앞서...제목, 내용, 이미지를 입력하는 페이지에서 내용을 입력할 때마다 사진이 계속 다시 렌더링 되어 깜박거리는 현
jjang-j.tistory.com
https://velog.io/@effypark/useEffect-feat.-clean-up
useEffect (feat. clean-up)
useEffect 를 사용할 때 `이전의 값을 가져오는 현상` 이 발생할 때 > React 의 useEffect 는 클래스형 컴포넌트의 라이프 사이클, 생명주기 메서드와 완전 같은 개념이 아니다. `componentDidMount` 와 유사하
velog.io
위에는 리액트 공식문서 안의 ChatRoom 예시인데요!
createConnection() API가 주어지며, 이 API는 connect() 및 disconnect() 메서드를 가진 객체를 반환합니다.
사용자에게 표시되는 동안 컴포넌트가 채팅 서버와의 연결을 유지하려면 어떻게 해야 할까요?
매번 재렌더링 후에 채팅 서버에 연결하는 것은 느리겠죠? 의존성 배열을 추가합니다.
엇 뭔가 이상하죠. 콘솔에 두 번 출력되네요.
이 문제는
1. ChatRoom 컴포넌트가 마운트되면 connection.connect()를 호출하여 서버와 연결을 시작합니다.
2.사용자가 다른 페이지로 이동하면 ChatRoom 컴포넌트가 언마운트되지만, 연결을 해제(connection.disconnect())하지 않으면 서버 연결이 남아있게 됩니다.
3. 사용자가 다시 ChatRoom으로 돌아오면 또다시 connection.connect()가 호출되어 중복 연결이 발생합니다.
이로 인해 연결이 계속 쌓여 메모리 누수, 성능 저하, 불필요한 리소스 소비 등의 문제가 발생할 수 있습니다.
바로 이 문제를 해결하기 위해! Effect 에서 클린업 함수를 반환하는 겁니다.
컴포넌트를 다시 마운트함으로써 리액트는 사용자가 다른 부분을 탐색하고 다시 돌아와도 코드가 깨지지 않을 것임을 확인하죠.
Effect 가 필요하지 않을 수도 있습니다
Effect는 React 패러다임에서 벗어날 수 있는 탈출구입니다. Effect를 사용하면 React를 “벗어나” 컴포넌트를 React가 아닌 위젯, 네트워크, 또는 브라우저 DOM과 같은 외부 시스템과 동기화할 수 있습니다.
예를 들어 일부 props 또는 state 가 변경될 때 컴포넌트의 state를 업데이트 하려는 경우 Effect 가 필요하지 않겠죠.
불필요한 Effect를 제거하면 코드를 더 쉽게 따라갈 수 있고, 실행 속도가 빨라지며, 에러 발생 가능성이 줄어듭니다.
Effect 가 필요하지 않은 두 가지 일반적인 경우가 있어요.
1. 렌더링을 위해 데이터를 변환하는 데
2. 사용자 이벤트를 처리하는 데
필요 없대요.
ex. (예제 많음 주의)
기존 props 나 state 에서 계산할 수 있는 것이 있으면 굳이 state 에 넣지 말고 렌더린 중에 계산하게 하자.
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 중복된 state 및 불필요한 Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
보기만 해도 복잡하죠. 이 코드는 만약 firstName 이 바뀌면 => 리렌더링 => useEffect => 리렌더링으로 2번의 렌더가 발생합니다.
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ 렌더링 중에 계산됨
const fullName = firstName + ' ' + lastName;
// ...
}
얘는 firstName이 바뀌면 => 리렌더링과 동시에 fullName 평가하겠죠?
Prop이 바뀌었을 때 모든 state를 key prop 을 이용하여 초기화 하기
간단히 커멘드를 작성할 수 있는 ProfilePage 컴포넌트를 예제로 들어볼게욥
userId가 바뀌면, comment 를 빈 문자열로 초기화 해야하는데 useEffect를 사용하면 다음과 같이 할 수 있습니다
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 Effect에서 prop 변경 시 state 초기화
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
🤔 앞서 봤던 예제처럼 불필요하게 리렌더링을 더 할 뿐만 아니라, ProfilePage가 다른 children 컴포넌트를 가지고 있다면 모두 일일이 초기화 해야 하겠죠?
즉, useEffect를 사용하면 비효율적일 뿐만 아니라 버그에 노출되기도 쉽습니다.
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ 이 state 및 아래의 다른 state는 key 변경 시 자동으로 재설정됩니다.
const [comment, setComment] = useState('');
// ...
}
key prop을 사용하면 훨씬 쉽게 컴포넌트를 초기화할 수 있어요😄
key는 본래 map을 사용할 때 컴포넌트를 구별하기 위해 전달해 주는 prop인데요
이를 역이용하여, key를 바꿔주면, React는 완전히 다른 컴포넌트로 인식하게 되어, 처음부터 다시 렌더링하게 됩니다!
Prop이 변경될 때 일부 state 조정하기
items가 바뀌었을 때 reverse는 가만히 두고, selection의 state만 변경하고 싶은 경우
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 Effect에서 prop 변경 시 state 조정하기
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
items가 변경되면
- 아직 업데이트되지 않은 selection으로 먼저 렌더
- useEffect 호출 후 새로운 selection으로 렌더
즉, 여태 봐왔던 예제들 처럼 stale state로 불필요한 렌더를 하고 있죠 😤
그런데 이번에는 조금 다른 방식으로 문제를 해결할 수 있습니다.
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 👍 렌더링 중 state 조정
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
이러면 앞서 봤던 것과 같이 렌더링하는 과정은 똑같지 않냐고 되물을 수 있습니다.
items가 바뀌었으니, setPrevItem(null)이 호출됬을 거고, 렌더한 이후에 바로 새로운 state로 렌더하게 되지 않을까?
하지만 useEffect를 사용했을 때와는 분명히 다르죠.
setState가 호출된다면 react는 새로 렌더링할 JSX 버리고, 새로운 state로 렌더링을 시도한다.
그러면 children에 대한 평가를 하지않아도 되기 때문입니다.
이벤트 핸들러 간 로직 공유
쇼핑몰 앱에서 상품에 장바구니를 담거나, 바로 구매하는 경우를 구현하려면
유저는 두 개의 다른 버튼을 클릭할 것이고, 우리는 handleBuyClick 이나 handleCheckoutClick 으로 product의 상태를 감지하여 모달을 띄워주겠죠?
function ProductPage({ product, addToCart }) {
// 🔴 Effect 내부의 이벤트별 로직
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}
하지만 아까 앞서 말했듯, useEffect 를 사용하면 안되는 경우 중 하나가 event 를 다룰 때 였잖아요.
ProductPage 컴포넌트에서 우리가 원하는 것은, 유저가 버튼을 클릭했을 때 모달이 띄워지는 것이지 product 라는 state 가 바뀌었기 때문에 모달이 띄워지는 것은 아니죠.
만약 유저가 새로고침을 한다면 useEffect 가 그대로 실행되면서, 버튼을 클릭하지도 않았는데 모달이 띄워지겠죠?
여기에서 공식문서는
어떤 코드가 Effect에 있어야 하는지 이벤트 핸들러에 있어야 하는지 확실하지 않은 경우 이 코드가 실행되어야 하는 이유를 자문해 보세요. 컴포넌트가 사용자에게 표시되었기 때문에 실행되어야 하는 코드에만 Effect를 사용하세요
라고 합니다.
따라서 이 예제에서는 이벤트 핸들러에서 즉각적으로 모달을 띄우는 코드를 호출하는 것으로 수정할 수 있겠죠.
function ProductPage({ product, addToCart }) {
// ✅ 이벤트 핸들러에서 이벤트별 로직이 호출됩니다.
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}
POST 요청 보내기
Form 컴포넌트에서 두 가지 useEffect 의 유즈케이스를 비교해볼까요?
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅컴포넌트가 표시되었으므로 이 로직이 실행되어야 합니다.
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
// 🔴 Effect 내부의 이벤트별 로직
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// ...
}
- 첫번째 useEffect: 유저에게 Form이라는 컴포넌트가 보여졌을 때 호출되어야 하므로, useEffect가 필요하다.
- 두번째 useEffect: 유저가 submit을 했을때(버튼을 클릭한다 던지). 즉, 인터랙션이 있으므로 useEffect로 처리해서는 안된다.
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ 컴포넌트가 표시되었으므로 이 로직이 실행됩니다.
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
function handleSubmit(e) {
e.preventDefault();
// ✅ 이벤트별 로직은 이벤트 핸들러에 있습니다.
post('/api/register', { firstName, lastName });
}
// ...
}
....등등등
진짜 많네요... 정리하다가 날 밤 새겄어.... 더 궁금하시면
https://ko.react.dev/learn/you-might-not-need-an-effect
Effect가 필요하지 않을 수도 있습니다 – React
The library for web and native user interfaces
ko.react.dev
ㅎㅎ...
반응형 effects 의 생명주기
이 파트는...면접에서 기술질문으로도 많이 나온다고 합니다! 그래서 조금 더 자세히 써봤어요~!
effects는 컴포넌트와 다른 생명주기를 가집니다.
컴포넌트 생명주기 | effect 생명주기 | |
시작 | 컴포넌트가 마운트 될 때 (화면에 추가 될 때) | 동기화가 시작될 때 (컴포넌트 마운트 시) |
작업 | 렌더링 및 UI 업데이트 | 비동기 작업 또는 DOM 변경 |
의존성 | props 및 state | 의존성 배열에 저장된 props 및 state |
중단 시점 | 컴포넌트가 언마운트 될 때 (화면에서 제거될 때) | 동기화 중지( cleanup 함수 호출 시) |
발생 횟수 | 마운트, 업데이트, 언마운트 시 각 1회 | 의존성 변경 시마다 여러 번 발생 가능 |
린트 규칙 | x | `eslint-plugin-react-hooks` 사용 |
의존성 최신화 | 필요 x | 의존성 배열에 최신 props 및 state를 지정 |
Cleanup | componentWillUnmount | return |
컴포넌트는 전체적인 생명주기(마운트, 업데이트, 언마운트 ) 를 고려해야 합니다. (위에서 봤죠?)
(componentDidMount, componentDidUpdate, componentWillUnmount)
이것들 맞습니다^^!
따라서 컴포넌트를 설계할 때는 "이 컴포넌트가 언제 렌더링 되고 언제 사라질까?" 를 생각하는 것이 자연스럽지만,
effect 는 컴포넌트의 생명주기와 독립적으로 동작하기 때문에, effect를 설계할 때는 "이 effect 가 언제 실행되고 언제 정리될까?" 를 독립적으로 고려해야 한다는 거죠.
Effect의 핵심 목적은 외부 시스템(브라우저 API, 타이머, 서버 등) 을 동기화 하는 것입니다. 코드가 변경되면 동기화를 더 자주 또는 덜 자주 수행해야 하는 것이죠.
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // -> 동기화 시작
connection.connect();
return () => {
connection.disconnect(); //-> 동기화 끝
};
}, [roomId]);
// ...
}
위 예제 코드에서 볼 수 있듯, effect의 본문에는 동기화 시작 방법과 effect에서 반환되는 cleanup 함수는 동기화를 중지하는 방법을 지정합니다,
그렇다면 동기화를 여러 번 시작하고 중지해야 할 수도 있겠죠?
밑에서는 왜 필요한지, 발생 시기, 그리고 이러한 동작을 제어할 수 있는 방법을 살펴보겠습니다!
동기화가 두 번 이상 수행되어야 하는 이유
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // "general" 방에 연결
connection.connect();
return () => {
connection.disconnect(); // "general" 방에서 연결 해제
};
}, [roomId]);
이 코드에서 "general" 대화방을 roomId 로 선택했다고 가정해 보면 지금까지는 아무 문제가 없겠죠?
그런데 이제 사용자가 "travel"을 선택하면 리액트가 UI를 업데이트 할거예요.
function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}
여기서 사용자는 UI에서 "travel" 이 선택된 대화방임을 알 수 있지만 지난번에 실행된 "general" 은 여전히 대화방에 연결되어 있겠죠? 하지만 roomId prop 이 변경되었기 때문에 그때 effect가 수행한 작업인 "general" 방에 연결이 더 이상 UI 와 일치하지 않습니다.
이때 리액트는 우리가 두 가지를 해주길 바라겠죠.
1. 이전 roomId 와의 동기화를 중지시키기, 즉 general 방에서 연결 끊기.
2. 새 roomId 인 travel 과 동기화 시작. 즉 travel 방이랑 연결하기
이 두 가지 방법을 실행하려면? 먼저 실행되었던 general 과의 동기화를 끊는 것이 먼저겠죠. 그 방법이 뭐랬어요?
cleanup! (동기화 중지 === cleanup 이렇게 묶어서 생각하면 편할 것 같은데요...🧐)
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // "general" 방에 연결
connection.connect();
return () => {
connection.disconnect(); // "general" 방에서 연결 해제
};
이러면 roomId 가 general 에서 travel 로 변경되어도 사용자가 선택한 방과 동일한 UI를 연결하겠죠~
그렇다면 여기서 컴포넌트의 관점과 effect 의 관점은 어떻게 다를까요!
컴포넌트 관점 | effect 관점 |
roomId 가 general 로 설정되어 "마운트" 된 ChatRoom | effect 가 "general" 대화방에 연결됨 |
roomId 가 travel 로 설정되어 "업데이트" 된 ChatRoom | "general" 방에서 연결 끊어지고 "travel" 방에 연결된 effect |
roomId 가 music 으로 설정되어 "업데이트" 된 ChatRoom | "travel" 방에서 연결 끊어지고 "music" 방에 연결된 effect |
언마운트 된 ChatRoom | "music" 방에서 연결이 끊어진 effect |
어떤 느낌인지 아시겠나요?
컴포넌트의 관점에서 보면 effect 를 "렌더링 후" 또는 "마운트 해제 전" 과 같은 특정 시점에 실행되는 '콜백' 또는 '생명주기 이벤트' 라고 생각하기 쉬웠지만 이런 생각을 멈춰!!
대신 항상 한 번에 하나의 시작/중지 사이클에만 집중하세요. 컴포넌트를 마운트, 업데이트 또는 마운트 해제하는 것은 중요하지 않습니다. 동기화를 시작하는 방법과 중지하는 방법만 설명하면 됩니다. 이 작업을 잘 수행하면 필요한 횟수만큼 effect를 시작하고 중지할 수 있는 탄력성을 확보할 수 있습니다.
그냥 화면에 뭐가 표시되어야 하는 지만 설명하면 나머지는 리액트가 알아서 한대요~
React가 이펙트가 다시 동기화될 수 있는지 확인하는 방법
React에서 effect가 다시 동기화될 수 있는지 확인하는 가장 일반적인 방법은 useEffect 훅의 두 번째 매개변수(dependency array, aka.의존성배열)를 사용하는 것입니다.
의존성 배열 안에 있는 변수가 변경될 때마다 useEffect가 실행되고 이를 통해 useEffect 내부의 코드가 해당 변수에 의존하는 상태를 항상 최신 상태로 유지할 수 있게 됩니다.
따라서 만약 이펙트가 다시 동기화될 수 있는지 확인하려면, useEffect의 의존성 배열에 해당하는 변수들을 변경하면서 해당 이펙트가 제대로 작동하는지 확인해야 합니다.
여기서! React 개발자 도구 사용법 잠깐 짚고 넘어갈게요~ (사실 제가 어떻게 쓰는 지 잘 몰라서...ㅎㅎ)
- React 개발자 도구를 열고 디버그하고자 하는 React 애플리케이션을 실행합니다.
- 웹 페이지에서 오른쪽 클릭을 하여 "React" 항목을 선택하고, "React 개발자 도구 열기"를 선택합니다.
- React 개발자 도구에서 "Components" 탭을 선택하고, 추적하려는 컴포넌트를 선택합니다.
- "Profiler" 탭을 선택하고, "Record" 버튼을 클릭하여 프로파일링을 시작합니다.
- 애플리케이션에서 해당 컴포넌트가 렌더링되는 시기에 이펙트가 실행되는지 확인합니다.
- 프로파일링을 멈추려면 "Stop" 버튼을 클릭합니다.
- 이펙트가 실행되는 시기와 관련된 정보를 확인하려면, "Flamegraph" 차트를 클릭하고, 해당 이펙트의 이름을 선택합니다.
- "Detail" 탭을 선택하여, 이펙트가 실행되는 시간과 관련된 정보를 확인할 수 있습니다.
React가 effect를 다시 동기화해야 한다는 것을 인식하는 방법
function ChatRoom({ roomId }) { // roomId prop은 시간이 지남에 따라 변경될 수 있습니다.
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 이 effect는 roomId를 읽습니다.
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // 의존성 배열 -> 리액트에 이 이팩트가 roomId에"의존"한다고 알려준다
roomId 가 변경되면 다시 동기화 되겠죠?
리액트가 이펙트를 다시 동기화해야 한다는 것을 인식하는 방법은 다음과 같습니다.
- 의존성 배열이 변경될 때: useEffect의 의존성 배열에 있는 변수가 변경될 때마다 이펙트가 다시 동기화됩니다. 이는 의존성 배열에 있는 변수가 업데이트될 때마다 이펙트를 재실행하여 최신 상태를 유지할 수 있도록 해줍니다.
- useEffect 내부에서 state나 props를 변경할 때: 이 경우, React는 이펙트 내부에서 발생한 변경사항이 컴포넌트의 렌더링에 영향을 미칠 수 있다고 판단하고 이펙트를 다시 동기화합니다. 따라서 useEffect 내부에서 state나 props를 변경하는 경우, 해당 변경사항이 필요한 경우에만 수행하도록 조건문을 추가하여 최적화할 수 있습니다.
- useEffect에 인자로 전달된 함수 내부에서 Promise나 async/await를 사용하는 경우: Promise나 async/await를 사용하여 데이터를 가져오는 경우, 이를 위해 비동기 처리를 수행하는 함수가 실행됩니다. 이 경우, React는 이펙트가 Promise나 async/await에서 반환된 데이터를 사용하므로 해당 데이터가 변경될 때 이펙트를 다시 동기화합니다.
각 effect 는 별도의 동기화 프로세스를 나타냅니다.
ex. 사용자가 회의실을 방문할 때 분석 이벤트를 전송하고 싶다고 가정. 이미 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로 분리하면 코드가 깔끔해 보이겠지만 유지 보수가 더 어려워집니다.
따라서 코드가 더 깔끔해 보이는지에 대한 여부가 아니라, 프로세스가 동일하거나 분리되어 있는지를 고려해야 합니다.
분명 린트 규칙까지 써놨는데 임시저장이 잘못됐나 없어졌어요 울고십다... 하지만 복습 오히려 좋아~! (ㅠㅠㅠ)
반응형 값에 “반응”하는 effect
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
위 코드에서는 두 개의 변수 severUrl , roomId 를 읽지만 종속성으로 roomId 를 지정했습니다.
그럼 여기서 serverUrl 이 종속성이 될 필요가 없는 이유는 뭘까요?
바로 재 렌더링으로 인해 serverUrl 이 변경되지 않기 때문입니다. 컴포넌트가 몇 번이나 다시 렌더링 하든 serverUrl 은 절대 변경되지 않겠죠? 그러니까 종속성으로 지정하는것은 의미가 없어요.
결국 종속성 === 시간이 지남에 따라 변경될 때만 무언가를 수행
빈 종속성이 있는 effect 의 의미
그럼 serverUrl 과 roomId 모두 컴포넌트 외부로 이동하면 어떻게 될까요?
const serverUrl = 'https://localhost:1234';
const roomId = 'general';
function ChatRoom(){
//같은 코드...
그럼 이제 의존성 배열이 비어있겠죠?
빈 의존성 배열은 effect가 컴포넌트에 마운트 될 때만 채팅방에 연결되고 컴포넌트가 마운트 해제될 때만 연결이 끊어진다는 것을 의미합니다. (여기서! 리액트는 로직을 스트레스 테스트 하기 위해 개발 단계에서 한 번 더 동기화 한다는 점을 기억!)
컴포넌트 본문에서 선언된 모든 변수는 반응형입니다.
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은 프로퍼티나 상태 변수가 아닙니다.
렌더링 중에 계산하는 일반 변수입니다.
하지만 렌더링 중에 계산되므로 재렌더링으로 인해 변경될 수 있습니다. 이것이 바로 반응형인 이유입니다.
컴포넌트 내부의 모든 값(컴포넌트 본문의 프롭, 상태, 변수 포함)은 반응형입니다.
모든 반응형 값은 다시 렌더링할 때 변경될 수 있으므로 반응형 값을 effect의 종속성으로 포함시켜야 합니다.
즉, effect 는 컴포넌트 본문의 모든 값에 "반응"합니다.
React 는 모든 반응형 값을 종속성으로 지정했는지 확인합니다.
린트 오류!
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) { // roomId는 반응형입니다.
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl는 반응형입니다.
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // <-- 여기 무언가 잘못되었습니다!
린터가 React 에 대해 구성된 경우, effect의 코드에서 사용되는 모든 반응형 값이 종속성으로 선언되었는지 확인합니다.
여기서 roomId 와 serverUrl 이 모두 반응형이기 때문에 린트 오류입니다.
리액트 오류처럼 보일 수 있지만 실제로는 코드의 버그를 지적하는 것이죠.
roomId 와 serverUrl 은 시간이 지남에 따라 변경될 수 있지만, 변경 시 effect 를 다시 동기화 하는 것을 잊어버리고 있습니다.
이러면 사용자가 UI 에서 다른 값을 선택한 후에도 초기 roomId와 serverUrl 에 연결된 상태로 유지하겠죠?
이 문제를 해결하려면 종속요소로 지정하면 됩니다.
}, [serverUrl, roomId]);
다시 동기화하지 않으려는 경우 어떻게 해야 하나요?
위의 예시에서는 종속성 배열에 roomId 와 serverUrl을 나열하여 린트 오류를 수정했었습니다.
하지만 대신 이러한 값이 반응형 값이 아니라는 것,
즉 재 렌더링의 결과로 변경될 수 없다 는 것을 린터에서 "증명" 할 수 있습니다.
해결할 방법은 다음과 같습니다.
1. effect 가 독립적인 동기화 프로세스를 나타내는지 확인하기
2. props 나 state 에 "반응" 하지 않고 effect 를 다시 동기화하지 않고 최신 값을 읽으려면 분리하면 됩니다!
-> effect를 반응하는 부분/ 반응하지 않는 부분 (추출 가능한 부분)
Effect 에서 이벤트 분리하기!
이벤트 핸들러는 같은 상호작용을 반복하는 경우에만 재실행 됩니다.
Effect 는 이벤트 핸들러와 달리 prop 이나 state 변수 등 읽은 값이 마지막 렌더링 때와 다르면 다시 동기화합니다.
위에서 언급한 effect 를 반응하는 부분과 반응하지 않는 부분, 즉 추출 가능한 부분을 구별하는 법을 알아봅시다.
이벤트 핸들러와 Effect 중에 선택하기
채팅방 컴포넌트를 구현한다고 생각해 볼까요?
1. 채팅방 컴포넌트는 선택된 채팅방에 자동으로 연결해야 한다.
2. "전송" 버튼을 클릭하면 채팅에 메시지를 전송해야 한다.
이벤트 핸들러는 특정 상호작용에 대한 응답으로 실행된다
여기서 사용자 관점에서 메시지는 "전송" 버튼이 '클릭' 되었기 때문에 전송되어야 합니다.
다른 때나 다른 이유로 메시지가 전송되면 사용자는 어? 클릭하지도 않았는데 전송이 되네..? 하고 당황하겠죠?
그러니까 메시지를 전송하는 건 이벤트 핸들러입니다.
Effect는 동기화가 필요할 때마다 실행된다
그럼 1번 요구사항인 "채팅방 컴포넌트는 선택된 채팅방에 자동으로 연결해야 한다"
이 코드를 실행하는 이유는 어떠한 특정 상호작용이 아닙니다.
채팅방 컴포넌트가 앱의 첫 화면이고 사용자가 아무런 상호작용을 하지 않은 경우라 해도 여전히 연결되어있어야 하기 때문에 effect 입니다.
반응형 값과 반응형 로직
이벤트 핸들러는 버튼 클릭과 같이 "수동" 으로 트리거, 하지만 effect 는 동기화 유지에 필요한 만큼 자주 실행 혹은 재실행 되기 때문에 "자동" 으로 트리거 됩니다.
이벤트 핸들러 | effect | |
트리거 | 수동 | 자동 |
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
}
컴포넌트 본문 내부에 선언된 props, state, 변수를 "반응형 값" 이라고 합니다.
위의 코드에서 serverUrl 은 반응형 값이 아니지만 roomId 와 message 는 반응형 값이죠?
반응형 값은 데이터 렌더링 과정에 관여합니다.
이벤트 핸들러 | effect | |
로직 | 반응형이 아님 - 사용자가 같은 상호작용( ex. 클릭) 을 반복하지 않는 한 재실행 x |
반응형 -값이 달라지면 다시 실행 |
Effect에서 비반응형 로직 추출하기
1. Effect 이벤트 선언하기
useEffectEvent Hook 사용하기
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('연결됨!', theme);
});
// ...
여기서 onConnected 를 Effect 이벤트라고 합니다. Effect 로직의 일부지만 이벤트 핸들러랑 훨씬 비슷하게 동작한다고 합니다!
하지만 Effect 의 의존성 목록에서 onConnected 를 제거해야 합니다. 왜?
Effect 이벤트는 반응형이 아니니까요!
2. Effect 이벤트로 최근 props 와 state 읽기
Effect 이벤트를 사용하면 억제하고 싶을 수 있는 많은 종속성 린터 패턴을 수정할 수 있습니다.
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 React Hook useEffect has a missing dependency: 'url'
// ...
}
이 코드에서 각 URL 은 서로 다른 페이지를 나타내므로 각 URL 에 대한 방문을 따로 기록하려고 합니다.
즉 logVisit 은 url 이 바뀔 때마다 바뀌는 반응형이어야겠죠?
이런 경우에는 의존성 린터의 말을 따라 url 을 의존성으로 추가하는 것이 합리적입니다.
}, [url]);
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect has a missing dependency: 'numberOfItems'
// ...
}
다음으로 모든 페이지 방문기록에 장바구니의 물건 개수도 포함하려고 하는 코드입니다.
Effect 내부에서 numberOfItems 를 사용했으므로 린터는 이를 의존성에 추가해달라고 하지만, logVisit 호출이 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은 Effect 이벤트이고, 그 내부의 코드는? 반응형일까요~ 아닐까요~
반응형이 아니죠?
그러므로 numberOfItems (또는 다른 반응형 값!)의 변경이 주변 코드를 재실행시킬 걱정 없이 사용할 수 있습니다.
반면에 Effect 자체는 여전히 반응형입니다. Effect 내부의 코드는 prop인 url을 사용하므로 다른 url로 리렌더링 될 때마다 Effect가 재실행됩니다. 그로 인해 Effect 이벤트인 onVisit가 호출될 것입니다.
결과적으로 prop인 url 변경될 때마다 logVisit을 호출할 것이고 항상 최근의 numberOfItems를 읽을 것입니다.
하지만 numberOfItems 혼자만 변경되면 어떠한 코드도 재실행되지 않습니다.
3. Effect 이벤트의 한계
Effect 이벤트는 사용할 수 있는 방법이 매우 제한적입니다.
1. Effect 내부에서만 호출하기
2. 절대로 다른 컴포넌트나 Hook 에 전달하지 말기.
const onTick = useEffectEvent(() => {
setCount(count + 1);
});
useTimer(onTick, 1000);
이렇게 Effect 이벤트 전달하지 말기!
Effect 이벤트는 Effect 의 코드 중 비반응형인 "부분" 이기 때문에 Effect 는 자신을 사용하는 Effect 근처에 있어야 합니다!
3. 객체와 함수를 종속성으로 사용하지 말기.
Effect 의존성 제거하기
불필요한 의존성으로 인해 Effect 가 너무 자주 실행되거나 무한 루프를 생성할 수도 있습니다. Effect 에서 불필요한 의존성을 어떻게 확인하고 제거하는지 이 페이지에서 알아볼게요~!
1. 의존성은 코드와 일치해야 합니다.
Effect를 작성할 때, 먼저 effect 가 수행해야 할 작업을 시작하고 중지하는 방법을 지정하는데, 이때 종속성을 비워두면, 린터가 올바른 종속성을 제안합니다.
그럼 그 종속성을 빈 배열에 추가하는거죠
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // 이게 비워져 있으면
//11:6 - React Hook useEffect has a missing dependency: 'roomId'. Either include it or remove the dependency array.
//라고 린트 오류가 뜸
// ...
}
Effect 는 반응형 값에 "반응" 함 -> roomId 는 반응형 값, 즉 재렌더링으로 인해 변경될 수 있고, 린터는 이를 의존성으로 지정했는지 확인함.
2. 의존성을 제거하려면 의존성이 아님을 증명하세요.
- Effect의 의존성을 "선택"할 수는 없다.
- Effect의 코드에서 사용되는 모든 반응형 값은 의존성 목록에 선언되어야 한다.
- 반응형 값에는 props와 컴포넌트 내부에서 직접 선언된 모든 변수 및 함수가 포함된다.
- 만약 의존성을 제거하려면 해당 의존성이 의존성 배열에 추가될 필요가 없다는 것을 린터에게 "증명"해야 한다.
ex) 위의 예제 코드에서 roomId 를 컴포넌트 밖으로 이동시켜서 반응형 값이 아니고 재렌더링 시에도 변경되지 않음을 증명할 수 있겠죠.
3. 의존성을 변경하려면 코드를 변경하세요.
- 먼저 Effect의 코드 또는 반응형 값 선언 방식을 변경합니다.
- 그런 다음, 변경한 코드에 맞게 의존성을 조정합니다.
- 의존성 목록이 마음에 들지 않으면 첫 번째 단계로 돌아갑니다. (그리고 코드를 다시 변경합니다.)
여기서 마지막 부분이 중요한데, 의존성을 변경하려면 먼저 주변 코드를 변경합시다.
의존성 목록 === Effect 코드에서 사용하는 모든 반응형 값의 목록

불필요한 의존성 제거하기
아까 예시에서 roomId는 의존성 배열에 있는 값이었죠? 이 의존성이 변경되면 Effect 가 다시 실행되는 것이 합리적인걸까요?
리액트 공식문서는 답이 참 애매한데요... 가끔 "아니오" 라고 합니다.
- 다른 조건에서 Effect의 다른 부분을 다시 실행하고 싶을 수도 있습니다.
- 일부 의존성의 변경에 “반응”하지 않고 “최신 값”만 읽고 싶을 수도 있습니다.
- 의존성은 객체나 함수이기 때문에 의도치 않게 너무 자주 변경될 수 있습니다.
이 코드를 이벤트 핸들러로 옮겨야 하나요?
가장 먼저 생각해보아야 할 것은 이 코드가 effect 되어야 하는지?
특정 이벤트(ex. 폼 제출) 에 따라 작동하는 로직은 useEffect 가 아닌 해당 이벤트 핸들러로 이동하는 것이 맞는 것처럼
고려해야 할 사항을 하나씩 생각해 보는 것이 좋습니다.
Effect 가 관련 없는 여러 가지 작업을 수행하나요?
즉 useEffect 에서 작업을 분리하는 것인데
서로 독립적인 두 가지 작업 (ex. 국가별 도시 목록과 도시별 지역 목록) 을 하나의 useEffect 에서 처리하지 말고, 각각 별도의 useEffect 로 분리하는 방법입니다.
왜? 서로 관련이 없는 두 가지를 동기화하는 것이 문제기 때문이죠~!
다음 State를 계산하기 위해 어떤 State를 읽고 있나요?
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...
messages State 변수를 업데이트 하는 Effect 입니다. 하지만 messages를 의존성으로 만들면 문제가 생기는데요
채팅 연결은 한 번만 설정되고 유지되어야 하는데 메시지가 추가될 때마다 연결을 초기화하면 과도한 리소스 사용과 연결 불안정 문제가 생길 수 있어요. ex) 서버의 연결 상태가 계속 끊길 수도 있다고 합니다.
이 문제를 해결하기 위해서 "업데이터 함수" 를 setMessages에 전달합시다.
setMessages(msgs => [...msgs, receivedMessage]);
이러면 useEffect 가 상태 변경으로 인해 불필요하게 다시 실행되지 않아요.
값의 변경에 ‘반응’하지 않고 값을 읽고 싶으신가요?
이 파트는 위에서 충분히 공부하셨다면 아셔야해요!!!!!
값이 변경될 때마다 Effect가 다시 동기화되고 연결되는건 좋은 게 아니라고 했죠?
비반응 로직을 이벤트로 옮기면 된다~!~!
props를 이벤트 핸들러로 감싸기
function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);
const onMessage = useEffectEvent(receivedMessage => {// 호출을 Effect 이벤트로 감싸기
onReceiveMessage(receivedMessage); //onReceiveMessage는 의존성,
//즉 재렌더링 될 때마다 Effect 가 다시 동기화되고, 채팅에 다시 연결되고...
//그럼 호출을 Effect 이벤트로 감쌉니다.
});
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage); //이벤트로 감싸면 뭐다?
});
return () => connection.disconnect();
}, [roomId]);
// ...
이벤트로 감싸면 뭐다 ? 더 이상 반응형이 아니게 되니까 의존성으로 지정할 필요가 없다~!
그래서 부모 컴포넌트가 재렌더링될 때마다 다른 함수를 전달하더라도 채팅이 더 이상 다시 연결되지 않겠죠.
사실 반응형 vs 비반응형을 구분하고 분리하는 게 다인 것 같아서.. 이 부분은 연습을 많이 해봐야겠다고 생각했습니다 -!

오우... 진짜진짜 양이 엄청 많죠...
하지만 진짜 의존성이 뭔지, 이벤트인지 Effect 인지 구분 잘 해주고 잘 넘겨주기만 하면 되는 (말은 쉽지...) 것 같습니다... ㄴㅔ..
다 읽으셨다면 정말정말 수고하셨구요. 양이 많은 만큼 필요한 부분만 뽑아 읽으셔도 좋을 것 같아요-!
아티클 읽으시다가 오류나, 궁금한 점 있으면 가감없이 피드백 남겨주시면 감사하겠습니다~!

린트 오류는... 4주차 과제 할 때도 많이 보였었는데요.
빨간 줄이 쳐진 곳에 커서를 갖다 대면 quick-fix 가 그럼 린터를 억제하는 effect 코드가 한 줄 ... 자동으로 타이핑 되었었는데,
그때는 어? 이제 잘 실행 되네? 하고 넘겼었는데 대왕 버그가 생길 수 있다니 처음부터 의존성인지,
아닌지를 잘 파악해야겠다고 생각했습니다.
다른 분들은 린트 오류 어떻게 해결하시는지가 궁금해요~!
이번 주차도 다들 너무너무너무 수고하셨습니다!!
'나야, 리액트 스터디' 카테고리의 다른 글
[week 7] - Effect가 필요하지 않은 경우 알아보기 (2) | 2024.12.08 |
---|---|
[Week7] Effect로 동기화하기 (2) | 2024.12.08 |
[week7] useEffect, customHook (2) | 2024.12.08 |
[Week 6] Ref (1) | 2024.12.02 |
[week6]탈출구 - Ref로 값 참조하기, Ref로 DOM조작하기 (2) | 2024.12.01 |