안녕하세요! 물결웹팟 OB 박채연입니다! 😋
7주차엔 Effect로 동기화하기 / Effect가 필요하지 않은 경우 부분을 읽고
Effect에 대해 딥다이브 하는 내용이었는데요!
useEffect을 필요할 때 적절하게 사용하란 이야길 많이 들었지만,
그 이유에 대해 깊게 파본 적은 없었던 것 같아서, 이번에 확실히 공부할 수 있었던 것 같아요!
그럼 바로! Effect에 대해 알아봐요! 💨
📍 Effect가 무엇일까
Effect를 사용하면, 렌더링 후 특정 코드를 실행하여, React 외부의 시스템과 컴포넌트를 동기화 할 수 있다.
Effect가 무엇인지부터 차근차근 알아보자 !
✨ 렌더링 코드를 주관하는 로직
- 컴포넌트의 최상단에 위치
- props와 state를 적절히 변형
- 결과적으로 JSX 반환
=> 순수해야 함 (결과만 계산하고, 그 외엔 아무것도 하지 말아야 함)
✨ 이벤트 핸들러
- 단순한 계산 용도가 아닌, 무언가를 하는 컴포넌트 내부의 중첩 함수
- 입력 필드를 업데이트하거나 / HTTP 요청을 보내거나 / 사용자를 다른 화면으로 이동시키는 등
- 특정 사용자 작업으로 인해 발생하는 부수 효과를 포함
✨ Effect
렌더링 자체에 의해 발생하는 부수 효과를 특정하는 것 !
특정 이벤트가 아닌, 렌더링에 의해 직접 발생하는 것을 말함
화면 업데이트가 이루어지고 나서 실행됨
주로 React 코드를 벗어난, 특정 외부 시스템과 동기화 하기 위해 사용 (브라우저 API, 네트워크 등)
VideoPlayer 컴포넌트 예시
import { useState, useRef } from 'react';
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 />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? '일시정지' : '재생'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
비디오 isPlaying 상태에 따라 비디오를 재생하고 멈추는 걸 구현하고 싶은데,
video 태그엔 isPlaying props가 없으므로, DOM 노드의 ref를 가져와야 한다.
하지만 위 코드에선 아래 두 가지 문제가 존재한다.
1️⃣ 렌더링 중에 DOM을 조작하기 때문에 런타임 에러가 발생한다.
리액트에선, 렌더링이 JSX의 순수한 계산이어야 하고, DOM 수정과 같은 부수효과를 포함하면 안되기 때문!
2️⃣ 처음으로 VideoPlayer가 호출될 때, 해당 DOM이 아직 존재하지 않는다.
리액트는 컴포넌트가 JSX를 반환할 때까지 어떤 DOM을 생성할지 모르기 때문에, play(), pause()를 호출할 DOM 노드가 아직 없음
부수 효과를 렌더링 연산에서 분리하기 위해 useEffect로 감싸면 해결된다.
import { useEffect, useRef } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
❗️ 무한루프 주의하기
Effect는 모든 렌더링 후에 실행되고, state를 설정하면 렌더링이 트리거 된다.
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
위 코드는 Effect가 실행되고, 상태가 설정되면 재렌더링이 발생하고, Effect가 다시 실행되고,
상태가 설정되면 또 다른 재렌더링이 발생하는 식으로 무한 루프에 빠지게 되는 것이다.
=> Effect 안에서 즉시 상태를 설정하지 말자 🐎
📍 의존성 지정하기
기본적으로 Effect는 모든 렌더링 후에 실행되지만,
useEffect의 두 번째 인자로 의존성 배열을 지정하면 해당 배열에서 변경사항이 발생했을 때만 실행된다.
의존성 배열엔 여러 개의 종속성을 포함할 수 있으며, 리액트는 Object.is 비교를 사용해 종속성 값을 비교한다.
useEffect(() => {
if (isPlaying) {
console.log('video.play() 호출');
ref.current.play();
} else {
console.log('video.pause() 호출');
ref.current.pause();
}
}, [isPlaying]);
비디오 플레이어 예시에서도 마찬가지로, isPlaying을 의존성 배열로 추가하면,
isPlaying의 값이 변경될 때만 useEffect가 실행된다.
ref의 경우, 리액트에서 동일한 useRef 호출에서 항상 같은 객체를 얻을 수 있음을 보장하기 때문에,
(= 이 객체는 절대 변경되지 않기 때문에) 자체적으로 Effect를 다시 실행하지 않음으로
의존성 배열에 포함하든, 하지 않든 상관 없다는 점!
❓ Object.is 비교
value1과 value2가 서로 같은 값인지 여부를 나타내는 Boolean 값을 반환한다.
객체 간의 비교를 실행할 땐, 참조 값을 비교하기 때문에, 같은 내용의 객체여도, 참조 값이 다르면 false를 반환한다.
Object.is(value1, value2);
❗️ 의존성 배열은 선택할 수 없다.
의존성 배열에 지정한 종속성이 Effect 내부의 코드를 기반으로
리액트가 기대하는 것과 일치하지 않으면, 린트 에러가 발생한다.
=> 의존성 배열에 해당 상태를 지정하고 싶지 않으면, Effect 내부를 수정하여 종속성이 필요하지 않도록 만들어야 한다.
❓ 의존성 배열이 없다면?
useEffect에 의존성 배열이 없다면 모든 렌더링 후에 실행된다.
[] 빈 배열로 의존성 배열을 지정할 경우엔, 컴포넌트가 마운트 될 때만 실행된다는 점!
useEffect(() => {
// 모든 렌더링 후에 실행됩니다
});
useEffect(() => {
// 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)
}, []);
useEffect(() => {
// 마운트될 때 실행되며, *또한* 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다
}, [a, b]);
📍 클린업 함수
컴포넌트가 언마운트되거나, useEffect가 실행되기 전에 호출하는 함수
컴포넌트가 마운트 해제될 때 연결을 닫는 과정을 위해 클린업 함수를 호출할 수 있음
Effect가 수행하던 작업을 중단하거나 되돌리는 역할을 하기 때문에 메모리 누수 방지를 위해 사용
뿐만 아니라, 컴포넌트가 언마운트 된 후에도 동작이 이뤄지는 경우, 예상치 못한 곳에서 의도하지 않은 동작이 발생할 수 있음
import { useState, useEffect } from 'react';
function createConnection() {
// 실제 구현은 정말로 채팅 서버에 연결하는 것
return {
connect() {
console.log('✅ 연결 중...');
},
disconnect() {
console.log('❌ 연결 해제됨');
}
};
}
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, []);
return <h1>채팅에 오신걸 환영합니다!</h1>;
}
📍 Effect가 항상 필요한 것은 아니라고?
- useEffect의 과도한 사용은 불필요한 렌더링, 복잡한 의존성 관리 문제를 야기할 수 있다.
- 상태와 렌더링 로직을 분리하지 않으면 의도치 않은 동작이 발생할 가능성이 높다.
리액트의 철학은 렌더링 중에는 "순수한 로직"만 처리하고, 실제 부수효과는 이후에 처리하는 것
useEffect는 부수효과만 처리해야 하며, 렌더링 중에 실행할 수 있는 작업은 Effect로 분리하지 않아야 한다!
❄️ Effect 없이 상태를 업데이트할 수 있는 경우
상태 업데이트 로직은 컴포넌트 렌더링 시 직접 실행하도록 만들어야 한다.
useEffect 없이도 상태 계산 로직은 이벤트 핸들러나 렌더링 중에 해결할 수 있습니다.
// 잘못된 예: 불필요한 useEffect
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// 올바른 예: 렌더링 중에 바로 계산
const fullName = `${firstName} ${lastName}`;
❄️ Effect로 초기 데이터를 가져오는 경우
데이터를 가져오는 로직은 대부분 Effect를 사용하지만, 항상 필요한 것은 X
초기 데이터는 useEffect 없이도 컴포넌트 외부에서 처리하거나, 서버 사이드 렌더링(SSR)을 사용하여 해결할 수 있다.
// 잘못된 예: 데이터 가져오기 로직을 useEffect 안에서만 처리
useEffect(() => {
fetchUserData().then(setUser);
}, []);
// 올바른 예: 데이터 로직을 컴포넌트 외부로 추출
const user = fetchUserData(); // SSR 또는 컴포넌트 외부에서 실행
❄️ Effect로 이벤트 핸들링을 설정하는 경우
종종 이벤트 리스너를 설정하거나 정리할 때 useEffect를 사용하는데,
리스너 등록이 꼭 Effect 내부에서 이루어져야 하는지 검토하고,
필요하지 않다면 이벤트 핸들러를 렌더링 시 바로 연결하는 방식으로 사용할 수 있다.
// 잘못된 예: 마우스 움직임 리스너를 useEffect에서 설정
useEffect(() => {
const handleMouseMove = (event) => console.log(event);
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// 올바른 예: 이벤트 핸들러를 컴포넌트 렌더링 시 바로 설정
function handleMouseMove(event) {
console.log(event);
}
❄️ Effect로 타이머를 설정하는 경우
타이머 로직은 이벤트 핸들러로 처리하거나 렌더링 시 실행할 수 있다.
// 잘못된 예: useEffect에서 타이머 설정
useEffect(() => {
const timer = setTimeout(() => setReady(true), 1000);
return () => clearTimeout(timer);
}, []);
// 올바른 예: 타이머를 함수 내부로 이동
function startTimer() {
setTimeout(() => setReady(true), 1000);
}
❄️ Effect로 상태 동기화를 구현하는 경우
상태 동기화를 위해 Effect를 사용하면, 의존성 배열의 관리가 복잡해진다.
=> 계산된 상태는 렌더링 중에 바로 계산할 수 있어 의존성 문제를 제거하며,
즉, 동기화 상태를 직접 계산하는 파생 상태로 만들면 Effect가 필요 없다는 것!
// 잘못된 예: useEffect로 상태 동기화
useEffect(() => {
setDerivedState(state1 + state2);
}, [state1, state2]);
// 올바른 예: 파생 상태를 직접 계산
const derivedState = state1 + state2;
// 비효율적인 동기화 방식
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// 계산된 상태 방식
const fullName = `${firstName} ${lastName}`;
❄️ Effect로 외부 API 호출을 제어하는 경우
API 호출 로직이 자주 변경되거나, 의존성을 잘못 관리해 불필요한 호출이 발생할 수 있는데,
그렇기 때문에 API 호출은 상태를 직접 변경하거나, 비동기 로직을 컴포넌트 외부로 이동해 처리합니다.
// 잘못된 예: useEffect로 의존성을 관리하며 API 호출
useEffect(() => {
async function fetchData() {
const data = await getData();
setData(data);
}
fetchData();
}, [dependency]);
// 올바른 예: API 호출을 핸들러로 이동
async function handleFetchData() {
const data = await getData();
setData(data);
}
결론적으로, Effect는 "필요할 때만" 사용하는 것이 핵심이다!
✔️ 렌더링 시 계산할 수 있는 로직은 컴포넌트 내부에서 처리할 것
✔️ 초기 데이터는 컴포넌트 외부에서 가져올 것
✔️ 이벤트 핸들러와 상태 계산은 직접 설정해서 사용할 것
'나야, 리액트 스터디' 카테고리의 다른 글
[week7]탈출구 - Effect로 동기화하기, Effect가 필요하지 않는 경우, 커스텀 Hook으로 로직 재사용하기 (3) | 2024.12.08 |
---|---|
[week 7] - Effect가 필요하지 않은 경우 알아보기 (2) | 2024.12.08 |
[Week7] Effect로 동기화하기 (2) | 2024.12.08 |
[week7] Effect& custom Hook (4) | 2024.12.08 |
[week7] useEffect, customHook (2) | 2024.12.08 |