home

Effect

글 분류
react bible
키워드
react
생성일
2023/04/30 13:07
최근 수정일
2023/11/01 23:53
작성중
클릭, 드래그와 같은 특정 이벤트가 아닌 렌더링으로 인해 생기는 side effects를 직접 지정할 수 있게 해주는 리액트 기능
effect는 commit 되어 화면이 업데이트 된 이후에 실행된다.
effect는 escape-hatch다. react 외부의 것과 연동되어야할 때, 내가 하려는 것에 대한 built-in 해결법이 존재하지 않을때.
리액트가 아닌것과의 연동에 사용한다 → non-react 위젯, 네트워크, 돔과 같은 것
“생각보다 Effect는 필요하지 않을 수도?” → Effect는 리액트 코드에서 벗어난 외부 라이브러리, API 연동과 같은 곳에 사용되는 것이다. 생각보다 Effect를 사용할 곳은 없다를 항상 명심하자.
현재 prop과 state에 맞게 외부 시스템을 동기화하는 방법이다. 더 깊게 생각하거나 더 이용하려 하지 말자. → effect 말고는 해결할 방법이 없다! 일 때만 사용하자.

기본

언제 실행 되는가?

컴포넌트가 DOM에 추가됐을 때
dependencies가 변경됐을 때 → cleanup 함수가 있을 경우 이전 값들을 이용해 cleanup 함수를 먼저 실행 후 setup 함수 실행
컴포넌트가 DOM에서 제거 될 때 → cleanup 함수 실행

주의사항

항상 컴포넌트의 top level에서 호출되어야 한다. loop이나 조건부로 호출 하고 싶을 경우 새로운 컴포넌트로 분리하자.
객체, 함수는 주소값을 참조한다. 당연하게도 리렌더링 간에 주소값은 계속 바뀌므로 객체, 함수를 dependency로 가지고 있는 effect는 실제 값 변경과 무관하게 리렌더링 시 항상 실행된다.
effect가 이용자 상호작용(클릭)으로 실행되지 않을 경우 업데이트된 브라우저 화면을 paint(React DOM이 수행하는 실제 브라우저에 그리는 단계)한 후 실행된다. → repainting하는게 싫다면 useLayoutEffect를 사용하자.
당연하게도, effects는 클라이언트에서 실행된다. 서버 렌더링에서는 실행되지 않는다.

dependency

dependency는 당신이 고르는게 아니다. 이미 넣어야할걸 넣어주는 것이다. dependency lint 에러는 컴파일 에러와 같이 생각해라.

dependency는 골라 담는게 아니다

dependency는 당신이 고르는게 아니다. lint에서 추가하란대로 해라.
지금 해당 Effect가 사용하고 있는 reactive 값들은 항상 dependency에 존재해야한다.
dependency가 현재 effect 코드와 매칭되지 않을 경우 해당 컴포넌트는 error prone일 확률이 높아진다. → 없는값을 참조한다던지, 최신화 되지 않은 값을 참조한다던지.
“아 dependency에 이거 들어가면 무한 렌더링 인데… 뺄까?” 라는 생각으로 빼지 말아라. 에러로 빠지는 지름길이다.
linter는 dependency에 빠진 reactive 값을 알려주는 것 이다.
linter가 틀린게 아니라 내가 틀린것. 함부로 ignore 하지마라. 린트는 항상 옳다.
디펜던시가 필요없는 없는 코드로 작성을 하던가

Object.is

리엑트에서 dependency의 변화 여부에 대한 비교는 Object.is 메서드로 수행한다.
Object.is(NaN, NaN); // true NaN === NaN; // false Object.is(-0, +0); // false -0 === +0; // true
TypeScript
복사
Object.is는 === 연산자, == 연산자 둘과 비교했을때 조금씩 다르다.
=== 연산자는 -0과 +0을 같은 값으로 다룬다.
=== 연산자는 NaN은 서로 다른 취급을 한다.
== 연산자는 형변환하나 Object.is는 형변환 하지 않는다.

ref, setState는 dependency가 되지 않아도 된다.

ref와 setState는 리액트가 리렌더간 같은 참조를 보장한다. 어차피 같은 객체라 렌더간 바뀌는 값이 아니므로 dependency에 추가될 필요가 없다. 이로인해 linter도 에러를 출력하지 않는다.
물론 넣어도 된다. 하지만 필요 없는 정보를 굳이 넣어야할까?
하지만 린트는 부모로 부터 전달받은 ref는 dependency에 넣는걸 추천한다. → linter가 어떤 값인지 모른다는건 실제로 보장되지 않은 값이란 뜻이다. 자식 입장에선 부모가 어떤 값을 넘겨주는지도 모르기 때문에 추가하는게 더 안전한 코드를 만든다.

애플리케이션이 시작될 때 딱 한번 실행되어야 하는 코드

if (typeof window !== 'undefined') { // Check if we're running in the browser. checkAuthToken(); loadDataFromLocalStorage(); } function App() { // ... }
TypeScript
복사
애플리케이션이 실행 될 때 딱 한번 실행되야 하는 코드는 useEffect가 아닌 최상위 컴포넌트(app.tsx) 외부에 작성한다.
app.tsx가 아닌 다른 컴포넌트에서도 컴포넌트 외부 최상단에 코드를 작성할 경우 렌더링 여부와 상관 없이 컴포넌트를 불러올 때 렌더링 여부와 상관 없이 딱 한번만 실행한다.

관련없는 로직의 effect를 다른 로직의 effect와 통합

dependency가 같다고 같은 effect에 전혀 다른일을 하는 로직을 추가할 경우 코드가 확장될 때 예기치 않은 에러를 마주칠 확률이 높아진다.
작성할 당시에는 코드가 더 줄어 보기 편한 코드라 생각 할 수 있지만 코드 확장 시(dependency가 늘어날 때) 문제가 발생한다.
같은 dependemcy를 갖고있다고 여러 로직을 한 effect에 넣지말고 하는 일에 따라 분리해야한다. 같은 dependency를 갖더라도 하는일이 다르다면 분리하여 관리해야한다. → 하는 일이 명확하지 않은데 엄청길다? → “잘못 만든 Effect가 아닌가 고민해보자.”
다른 Effect가 사라지더라도 작동에 문제없는 Effect를 만들어라.

effect’s lifecycle

컴포넌트의 시점
effect의 시점
ChatRoom 컴포넌트가 roomId(”general”) prop과 함께 마운트 됐다.
effect가 general에 연결됐다.
ChatRoom 컴포넌트의 roomId prop이 “travel”로 업데이트 됐다.
effect가 “general”로 부터 연결해제 되었고 “travel”로 연결되었다.
ChatRoom 컴포넌트의 roomId prop이 “music”로 업데이트 됐다.
effect가 “travel”로 부터 연결해제 되었고 “music”으로 연결되었다.
ChatRoom 컴포넌트가 언마운트 됐다.
effect가 music으로 부터 연결이 해제 되었다.

컴포넌트 라이프사이클과는 다르다

대부분의 블로그에서는 “컴포넌트 마운드될 때, 언마운트 될 때 그리고 dependency가 바뀔때 실행되요~" 또는 “state 변화의 callback”과 같이 컴포넌트 관점에서 effect를 설명하지만 사실은 둘다 따로 분리하여 이해해야 한다.
effect의 lifecycle은 컴포넌트의 lifecycle(마운트, 업데이트, 언마운트)와 연결하여 생각하지 말고 effect의 synchronization의 시작과 끝을 주시해야한다. synching이 시작될때의 코드(setup) 끝날때의 코드(cleanup)만 신경써서 작성해주면 된다. 억지로 컴포넌트 라이프사이클과 매치하여 생각할 경우 코드의 가독성은 떨어지고 불필요 코드들이 작성된다.

렌더링이 두번되는데?

개발 모드에선 <StrictMode> 때문에 발생하는 기본 현상이다. 걱정하지 말자.
“어떻게 한번만 실행되게 하지?”가 아니라 “리마운트 후 에도 정상작동하게 하려면 어떻게 하지?”가 맞는 접근법 이다. → setup → cleanup → setup이 실행 되더라도 UI나 애플리케이션의 동작의 변화를 유저가 알아차려선 안된다.
이용자가 한번 렌더된 결과물(production 코드)과 한번 더 setup, cleanup된 결과물(dev 코드)의 차이를 느껴선 안된다.
리액트에서 모든 컴포넌트는 pure function이라 가정한다. 리액트 컴포넌트는 같은 input에는같은 같은 결과물이 나와야 한다는 뜻이다. → 렌더링을 두번하여 두 결과물이 같은지 비교한다.
cleanup에서도 해당된다. 두번 실행하여 문제점을 개발 당시에 파악하가 쉽게 만들기 위해 setup + cleanup 즉 한번 더 사이클을 실행한다.
개발 모드에선 두번 실행되는게 맞다.(컴포넌트가 pure한지 알기 위해) → cleanup을 잘 적용했다면 한번 실행되던 두번 실행되던 문제가 발생하지 않는다.(product 코드에선 한번만 실행되어야 한다.)

analytics 전송

GA와 같이 페이지 방문 시 보내는 analytics 전송은 개발 모드에서 두번 실행된다.
결과적으로 코드 그대로 남겨두는걸 추천한다. 한번 렌더되건 두번 렌더되건 유저가 알수있는 차이점이 없기 때문이다. 애초에 개발자 모드에서 두번 실행되는게 싫다면 개발 모드에서 analytics 수집 코드를 제거하는게 맞는 접근법이다.

Effect는 생각보다 쓸일이 없다

리액트 외부에 존재하는 것들에선 effect를 사용해야 겠지만 리액트 내부의 것들에선 굳이 사용할 이유가 없다는걸 기억하자.
effect는 컴포넌트를 연산한 다음 리액트가 commit 후 화면을 업데이트한 후 실행된다. 만약 effect에 state를 업데이트 하는 로직이 존재할 경우 처음부터 또 다시 실행한다.
effect를 사용할때는 effect를 사용해야만 하는 이유가 필요한 것 이다. 남발하지말고 최대한 사용하지 않겠다 라는 자세로 effect를 사용하자.

effect를 쓸까? event handler를 쓸까?

effect는 클릭이나 드래그 같은 특별한 이벤트에 대응하기 위한게 아닌 렌더링의 side effect를 위해 사용하는 것 이다. → 풀어 말하자면 이용자 상호작용 fire가 아닌 페이지 방문에 의한 fire
“변하는 값에 반응적으로 실행되야 하는 로직이 존재하는가?”
YES → Effect
NO → Event Handler
const connection = createConnection(serverUrl, roomId); connection.connect();
TypeScript
복사
roomId가 바뀔 때 마다 connection은 다시 이루어져야 한다. → roomId는 reactive value이며 이 값에 변화에 따라 로직이 실행되어야 할 경우 effect를 이용하면 된다.

effect를 사용해야하는 예시

예시 1. 채팅 페이지
버튼 클릭으로 인한 메시지 전송은 event로 추가되어야 한다
페이지에 들어갔을 때 server connecntion은 effect로 추가되어야 한다.
예시 2. ref로 DOM 노드의 선택이 필요할 경우
페이지가 첫 렌더링 되는 당시에는 DOM 노드를 useRef로 참조 할 수 없다. 렌더할 당시에는 DOM 노드에 존재하지 않기 때문이다.
전부다 화면에 그려진 후 DOM 노드를 참조한 useRef가 수행되어야할 경우에는 useEffect가 맞다.
이하 useEffect를 사용하지 않고 구현하는 방법에 대한 예시를 활용한 설명이다.

데이터 변환을 위한 effect

// AS-IS 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]); // ... } // TO-BE function Form() { const [firstName, setFirstName] = useState('Taylor'); const [lastName, setLastName] = useState('Swift'); // ✅ Good: calculated during rendering const fullName = firstName + ' ' + lastName; // ... }
TypeScript
복사
두가지 state를 조합하여 출력해야하는 예시가 있다. effect를 이용하여 작성할 수 도 있지만 이 방법은 비효율적인 방법이다.
굳이 effect를 추가하지 않고 컴포넌트 상단에 변환하는 로직을 추가할 경우 의미없는 effect 제거로 인한 가독성 향상 및 퍼포먼스 향상이 이루어진다.

이용자 상호작용 처리를 위한 effect

이용자의 상호작용으로 API를 post 하고 유저에게 알림을 보내는 케이스의 경우 effect에 추가될 이유가 없다. effect가 아닌 event handler에 이벤트 처리할 경우 경우 가독성 향상 및 퍼포먼스 향상이 이루어진다.

prop이 변경될 경우 내부 state 초기화

// AS-IS export default function ProfilePage({ userId }) { const [comment, setComment] = useState(''); // 🔴 Avoid: Resetting state on prop change in an Effect useEffect(() => { setComment(''); }, [userId]); // ... } // TO-BE export default function ProfilePage({ userId }) { return ( <Profile userId={userId} key={userId} /> ); } function Profile({ userId }) { // ✅ This and any other state below will reset on key change automatically const [comment, setComment] = useState(''); // ... }
TypeScript
복사
prop이 변경될 경우 내부 state를 변경해야하는 케이스가 존재한다.
effect를 이용하여 컴포넌트 내부 state를 수정할 경우 stale 값을 이용하여 렌더한 후 다시 렌더링하기 떄문에 비효율적이며 가독성이 떨어진다.
리액트 UI Tree의 특성을 이용해 부모 컴포넌트에서 key prop을 이용하여 해당 컴포넌트를 UI Tree상 다른 컴포넌트로 인식 시켜줄 경우 가독성과 퍼포먼스가 모두 향상된다. → UI Tree 상 다른 컴포넌트 일 경우 컴포넌트 내부 state가 모두 초기화 된다는 것을 기억하자.

prop 변화에 따른 내부 state 변화

// AS-IS function List({ items }) { const [isReverse, setIsReverse] = useState(false); const [selection, setSelection] = useState(null); // 🔴 Avoid: Adjusting state on prop change in an Effect useEffect(() => { setSelection(null); }, [items]); // ... } // Better than AS-IS function List({ items }) { const [isReverse, setIsReverse] = useState(false); const [selection, setSelection] = useState(null); // Better: Adjust the state while rendering const [prevItems, setPrevItems] = useState(items); if (items !== prevItems) { setPrevItems(items); setSelection(null); } // ... } // TO-BE function List({ items }) { const [isReverse, setIsReverse] = useState(false); const [selectedId, setSelectedId] = useState(null); // ✅ Best: Calculate everything during rendering const selection = items.find(item => item.id === selectedId) ?? null; // ... }
TypeScript
복사
부모로 부터 받은 prop을 기반으로 내부 state의 초기화나 수정이 필요한 케이스이다.
stale 값을 기반으로 렌더 후 effect로 인해 다시 렌더하는 케이스다. 위의 예시들과 똑같이 의미없는 렌더링이 여러번 일어난다.
better thans AS-IS
렌더링 중 컴포넌트를 업데이트할 경우 리액트는 생성중이던 JSX를 바로 버리고 다시 렌더링 한다. useEffect 보다 효율적이긴 하지만 props로 받았던 값을 state로 저장하여 다른 data flow를 만든다는 것 자체가 데이터 흐름 이해와 디버깅을 어렵게 만든다
TO-BE
렌더링 도중에 값을 결정하므로 분리된 내부 state의 만료에 대한 걱정도 없으며 여러 횟수의 렌더링이 일어나지 않아 퍼포먼스 적으로도 좋으며 가독성 또한 좋다.

중복된 event handler 함수 처리

// AS-IS function ProductPage({ product, addToCart }) { // 🔴 Avoid: Event-specific logic inside an Effect useEffect(() => { if (product.isInCart) { showNotification(`Added ${product.name} to the shopping cart!`); } }, [product]); function handleBuyClick() { addToCart(product); } function handleCheckoutClick() { addToCart(product); navigateTo('/checkout'); } // ... } // TO-BE function ProductPage({ product, addToCart }) { // ✅ Good: Event-specific logic is called from event handlers function buyProduct() { addToCart(product); showNotification(`Added ${product.name} to the shopping cart!`); } function handleBuyClick() { buyProduct(); } function handleCheckoutClick() { buyProduct(); navigateTo('/checkout'); } // ... }
TypeScript
복사
두개의 버튼(구매, 결제)이 있는 상품 페이지이며 두개의 버튼 중 하나를 클릭하면 notification을 보여줘야한다. 중복되는 로직을 줄이기 위해 effect에 notification을 추가하는 로직을 추가했다.
역시나 불필요함과 동시에 에러에 취약한 effect다.
구매 다하고 해당 페이지를 새로고침하거나 다시 방문할 경우 유저에게 알림이 다시 전송된다.
Effect는 컴포넌트가 유저에게 노출 됐을때 보여야만 하는 코드를 추가할때만 사용해야 한다.
Event hanlder에서 알림을 출력할 경우 에러도 제거되며 가독성도 좋아진다.

외부 데이터 저장소 구독

// AS-IS function useOnlineStatus() { // Not ideal: Manual store subscription in an Effect const [isOnline, setIsOnline] = useState(true); useEffect(() => { function updateState() { setIsOnline(navigator.onLine); } updateState(); window.addEventListener('online', updateState); window.addEventListener('offline', updateState); return () => { window.removeEventListener('online', updateState); window.removeEventListener('offline', updateState); }; }, []); return isOnline; } function ChatIndicator() { const isOnline = useOnlineStatus(); // ... } // TO-BE function subscribe(callback) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; } function useOnlineStatus() { // ✅ Good: Subscribing to an external store with a built-in Hook return useSyncExternalStore( subscribe, // React won't resubscribe for as long as you pass the same function () => navigator.onLine, // How to get the value on the client () => true // How to get the value on the server ); } function ChatIndicator() { const isOnline = useOnlineStatus(); // ... }
TypeScript
복사
외부 데이터 저장소 → navigator.onLine과 같은 브라우저 내장 API 또는 써드파티 라이브러리
이런 상황에서 effect를 사용하는게 일반적이지만 리액트에는 이 목적을 위해 내장된 hook이 존재한다. → 외부 저장소를 구독하여 값의 변화를 추적할 수 있는 훅

미래

With time, the React team’s goal is to reduce the number of the Effects in your app to the minimum by providing more specific solutions to more specific problems.
effects는 react에 있어서 탈출해치와 비슷한 존재다. react 밖에서 존재하는 무언가와 연동을 하거나 reactive value와 연동된 작업을 수행하는것과 같은 행위에서는 이만큼 좋은 해결책은 아직 없다.
하지만 react 팀의 목표는 “react 기반 웹 애플리케이션에서 사용되는 effects의 숫자를 줄이고 특정 문제에 대한 구체적인 해결법을 제공하는 방법으로 react 라이브러리를 업데이트 해갈 예정”이다.
effects로 모두 해결하려 했던 문제를 지금은 useSyncExternalStore와 같은 구체적인 해결법을 제공한다.

참조