Notice
Recent Posts
Recent Comments
Link
«   2026/02   »
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
Archives
Today
Total
관리 메뉴

효뚜르팝의 Dev log

[React 공식문서로 공부하기4] Escape Hatches 탈출구 본문

TIL

[React 공식문서로 공부하기4] Escape Hatches 탈출구

hyodduru 2024. 3. 4. 22:31
리액트 공식문서를 읽고 정리한 글입니다.
https://react-ko.dev/learn/escape-hatches

Escape Hatches

1. Referencing Values with Refs 

  • 컴포넌트가 특정 정보를 '기억'하도록 하고 싶지만 해당 정보가 새 렌더링을 촉발하지 않도록 하려는 경우 ref를 사용할 수 있다. 
  • ref는 React가 추적하지 않는 컴포넌트의 비밀 주머니와 같다. 
  • ref는 state와 달리 current 속성을 읽고 수정할 수 있는 일반 Javascript 객체. 

refs와 state의 차이점

  • refs : 변경 시 리렌더링 촉발 X, Mutable - 렌더링 프로세스 외부에서 current 값을 수정하고 업데이트 할 수 있음. 렌더링 중에는 current 값을 읽거나 쓰지 않아야 함. 
  • state : 변경 시 리렌더링 촉발, Immutatble - state setting 함수를 사용하여 state 변수를 수정해 리렌더링을 대기열에 추가해야 함, 언제든지 state를 읽을 수 있음. 각 렌더링에는 변경되지 않는 자체 state snapshot이 있음. 

ref는 내부에서 어떻게 동작하나? 

  •  ref를 설정자가 없는 일반 state 변수라고 생각하면 된다. useRef는 항상 동일한 객체를 반환함. 

ref를 사용해야 하는 경우

  • 일반적으로 ref는 컴포넌트가 React로부터 "외부로 나가서" 외부 API 즉, 컴포넌트의 형상에 영향을 주지 않는 브라우저 API  등과 통신해야 할 때 사용한다. ex) timeout ID 저장, 다음 페이지에서 다룰 DOM elements 저장 및 조작, JSX를 계산하는 데 필요하지 않은 다른 객체 저장 

ref의 모범 사례 

  • ref를 탈출구로 취급하라. ref는 외부 시스템이나 브라우저 API로 작업할 때 유용. 
  • 렌더링 중에는 ref.current를 읽거나 쓰지 마라. 렌더링 중 일부 정보가 필요한 경우 대신 state를 사용하라.

Ref와 DOM

  • ref는 모든 값을 가리킬 수 있다. 그러나 ref의 가장 일반적인 사용 사례는 DOM 요소에 엑세스 하는 것. ex) input에 focus를 맞추고자 할 때 

2. Manipulating the DOM with Refs 

다른 컴포넌트의 DOM 노드에 접근하기 

React는 컴포넌트가 다른 컴포넌트의 DOM 노드에 접근하는 것을 허용하지 않음. 

대신, DOM 노드를 노출하길 원하는 컴포넌트에 ref를 사용할 수 있도록 설정해야 함. 

const MyInput = forwardRef((props, ref) => {
	return <input {...props} ref={ref} />
    });

 

참고) 

useImperativeHandle을 사용하여 원본 DOM 엘리먼트를 노출하지 않을 수 있다. 

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Only expose focus and nothing else
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});

 

MyInput 내부의 realInputRef는 실제 input DOM 노드를 보유한다. 

하지만 useImperativeHandle은 부모 컴포넌트에 대한 ref 값으로 고유한 특수 객체를 제공하도록 React에 지시. 이 경우 ref "핸들"은 DOM 노드가 아니라 useImperativeHandle() 내부에서 생성한 사용자 정의 객체. 

React가 ref를 첨부할 때

React에서 모든 업데이트 단계 

1. 렌더링 : 렌더링하는 동안 React는 컴포넌트를 호출하여 화면에 무엇이 표시되어야 하는지 파악

2. 커밋(commit) : React는 DOM에 변경사항을 적용 

 

  • 렌더링 중에는 아직 DOM 노드가 아직 생성되지 않았으므로 ref.current는 null이 된다. 그리고 렌더링 하는 동안에도 DOM 노드가 아직 업데이트가 되지 않았기 때문에 읽기에는 이름. 
  • React는 커밋하는동안에 ref.current를 설정한다. DOM이 업데이트 되기 전에는 ref.current의 값을 null로 설정하였다가 DOM이 업데이트된 직후 해당 DOM 노드로 다시 설정함. 
  • 일반적으로 이벤트 핸들러에서 ref에 접근.

참고) flushSync로 state를 동기적으로 update 하기 

react-dom에서 flushSync를 import하고 state 업데이트를 flushSync 호출로 감싸면 React가 DOM을 동기적으로 업데이트 하도록 강제할 수 있음. 

flushSync(() => {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

 

React가 관리하는 DOM 노드를 변경하지 말 것. 하지만 React가 업데이트할 이유가 없는 DOM의 일부는 안전하게 수정할 수 있음.

3. Synchronizing with Effects 

React 컴포넌트 내부의 두가지 유형의 논리 

  • 렌더링 코드는 컴포넌트의 최상위 레벨에 있음. 여기서 props와 state를 가져와 변환하고 화면에 표시할 JSX를 반환한다. 렌더링 코드는 순수해야 함. 
  • 이벤트 핸들러는컴포넌트 내부에 있는 중첩된 함수로 계산만 하는 것이 아니라 별도의 작업도 수행함. 특정 사용자 작업(ex 버튼 클릭 또는 입력)으로 인해 발생하는 사이드 이펙트가 포함되어 있음. 

Efffect를 사용하면 특정 이벤트가 아닌 렌더링 자체로 인해 발생하는 사이드 이펙트를 명시할 수 있음. ex) 채팅 버튼을 클릭할 때마다 발생하는 이벤트 (이벤트와 달리 Effect는 특정 상호 작용이 아닌 렌더링 자체에 의해 발생하기 때문) 

Effect 작성 방법 

1. Effect를 선언한다. 

import { useEffect } from 'react';

function MyComponent {
	useEffect(() => {
    	// 여기의 코드는 매 랜더링 후에 실행이 된다. 
});

return <div />;
}

 

  • Effect는 렌더링의 결과로 실행된다. 
  • 컴포넌트가 렌더링될 때마다 React는 화면을 업데이트하고 useEffect 내부의 코드를 실행한다. 즉, useEffect는 해당 렌더링이 화면에 반영이 될 때까지 코드 조각의 실행을 “지연”한다. 

 

2. Effect의 의존성을 지정하기

React가 불필요하게 Effect를 다시 실행하지 않도록 지시할 수 있다. 

  • 의존성을 “선택”할 수 없다. 지정한 의존성들이 Effect 내부의 코드를 기반으로 React가 예상하는 것과 일치하지 않으면 lint 오류가 발생하고, 이는 코드에 버그를 잡는데 도움이 된다. 
  • 의존성 배열에서 ref는 생략된다. -> ref 객체가 안정적인 정체성을 가지고 있기 때문.

3. 필요한 경우 클린업 추가하기

  • React는 Effect가 다시 실행되기 전에 매번 클린업 함수를 호출하고, 컴포넌트가 마운트 해제(제거)될 때 마지막으로 한 번 더 호출한다. 

개발 환경에서 두 번씩 실행되는 Effect를 처리하는 방법 

올바른 질문은 "어떻게 하면 Effect를 한번만 실행할 수 있는가"가 아니라 "어떻게 다시 마운트한 후에도 Effect가 잘 작동하도록 수정하는가"이다. 일반적으로 클린업 함수를 통해 해결할 수 있다. 

  • React가 아닌 위젯 제어하기 

React로 작성하지 않은 UI 위젯을 추가해야하는 경우, 그 중 일부 API는 연속으로 두 번 호출하는 것을 허용하지 않을 수 있음. ex) <dialog />의 showModal method

  • Effect가 이벤트를 구독하는 경우 클린업 함수는 구독을 취소해야 함. 
useEffect(() => {
  function handleScroll(e) {
    console.log(window.scrollX, window.scrollY);
  }
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);
  • Effect가 무언가를 애니메이션하는 경우 클린업 함수는 애니메이션을 초기값으로 재설정해야 함
useEffect(() => {
  const node = ref.current;
  node.style.opacity = 1; // 애니메이션 촉발
                          
  return () => {
    node.style.opacity = 0; // 초기값으로 재설정
                            
  };
}, []);

 

  • Effect가 무언가를 페치하면 클린업 함수는 페치를 중단하거나 그 결과를 무시해야해야 함. 
useEffect(() => {
  let ignore = false;

  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }

  startFetching();

  return () => {
    ignore = true;
  };
}, [userId]);

 

  • 애플리케이션 초기하기와 같이 애플리케이션이 시작될 때 한 번만 실행되어야 하는 로직은 컴포넌트 외부에 넣을 수 있다. 
if (typeof window !== 'undefined') { // 실행환경이 브라우저인지 여부 확인
                                 
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}
  • React는 항상 다음 렌더링의 Effect 전에 이전 렌더링의 Effect를 정리한다. 

4. You Might Not Need an Effect 

Effect가 필요하지 않은 두가지 경우

  • 렌더링을 위해 데이터를 변환하는 경우 Effect는 필요하지 않다. 
    ex) 목록이 변경될 때 state 변수를 업데이트하는 Effect를 작성할 때
          -> 이는 비효율적임. 변경 사항을 DOM에 commit하여 화면을 업데이트하고, 그 후에 Effect를 실행함. 만약 Effect 역시 state를 즉시 업데이트 한다면 이로인해 전체 프로세스가 처음부터 다시 시작됨. 불필요한 렌더링을 피하려면 모든 데이터 변환을 컴포넌트의 최상위 레벨에서 할 것. 그러면 props나 state가 변경될 때마다 해당 코드가 자동으로 다시 실행된다. 
  • 사용자 이벤트를 처리하는 데에 Effect는 필요하지 않다. 
    -> Effect는 사용자가 무엇을 했는지 (ex 어떤 버튼을 클릭했는지)를 알 수 없음. 

Props 또는 state에 따라 state 업데이트하기 

기존 props나 state에서 계산할 수 있는 것이 있으면 state에 넣지 말 것. 대신 렌더링 중에 계산하기 

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // 렌더링 과정 중에 계산할 것 (state로 따로 만들 필요 없음) 
  const fullName = firstName + ' ' + lastName;
  // ...
}

고비용 계산 캐싱하기

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // getFilteredTodos()가 느리지 않다면 괜찮음. 따로 Effect와 state를 사용해서 업데이트해주지 않아도 됨
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

 

  • getFilteredTodos()가 느리거나 todos가 많을 경우 newTodo 와 같이 관련 없는 state 변수가 변경되더라도 getFilteredTodos()를 다시 계산하고 싶지 않을 수 있음 -> useMemo 훅으로 감싸서 캐시(memoize) 할 수 있다.
  • useMemo를 통해 todos나 filter가 변경되지 않는 한 내부 함수가 다시 실행되지 않기를 원한다는 것을 React에 알린다.
  • useMomo로 감싸는 함수는 렌더링 중에 실행되므로 순수 계산에만 작동한다. 

prop이 변경되면 모든 state 재설정하기 

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ key가 변하면 이 컴포넌트 및 모든 자식 컴포넌트의 state가 자동으로 재설정됨
  const [comment, setComment] = useState('');
  // ...
  
   // 🔴 이러지 마세요: prop 변경시 Effect에서 state 재설정 수행 
    useEffect(() => {
    setComment('');
  }, [userId]);
}

 

props가 변경될 때 일부 state 조정하기

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // ✅ 가장 좋음: 렌더링 중에 모든 값을 계산
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

이벤트 핸들러 간의 로직 공유 

  • 어떤 코드가 Effect에 있어야 하는지 이벤트 핸들러에 있어야 하는지 확실치 않은 경우, 컴포넌트가 사용자에게 표시되었기 때문에 실행되어야 하는 코드에만 Effect를 사용할 겻 
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 });
  }
  // ...
}

 

  • 여러 컴포넌트의 state를 업데이트해야 하는 경우 단일 이벤트에서 처리하는 것이 좋음 
  • 여러 컴포넌트에서 state 변수를 동기화하려고 할 때마다 state 끌어올리기를 고려할 것 
  • Effect로 데이터를 페치할 수 있지만 경쟁 조건을 피하기 위해 클린업 로직을 구현해야 함. 
    -> 경쟁 조건 : 서로 다른 두 요청이 서로 “경쟁”하여 예상과 다른 순서로 도착한 경우

외부 스토어 구독하기 

  • React에는 외부 저장소를 구독하기 위해 특별히 제작된 훅 useSyncExternalStore이 있음. 
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function useOnlineStatus() {
  // ✅ 좋습니다: 빌트인 훅에서 외부 store 구독
  return useSyncExternalStore(
    subscribe, // React는 동일한 함수를 전달하는 한 다시 구독하지 않음
    () => navigator.onLine,  // 클라이언트에서 값을 가져오는 방법
    () => true // 서버에서 값을 가져오는 방법
  );
}

 

5. Lifecycle of Reactive Effects

컴포넌트와 Effect의 생명 주기의 차이 

  • 컴포넌트 : 마운트, 새로운 props나 state를 받으면 업데이트, 화면에서 제거되면 컴포넌트 마운트 해제 
  • Effect : 외부 시스템을 현재 props 및 state에 동기화를 시작하고 나중에 동기화를 중지함 
function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect(); // 동기화를 시작함 
    return () => {
      connection.disconnect(); // 동기화를 중단함 
    };
  }, [roomId]);
  // ...
}

 

컴포넌트가 마운트될 때 동기화를 시작하고 마운트 해제될 때 동기화를 중지하는 것이 아니라, 때로는 컴포넌트가 마운트 된 상태에서 동기화를 여러 번 시작하고 중지해야 할 수도 있음. (컴포넌트의 생명주기와 Effect 생명주기를 분리해서 생각할 것) 

 

 

  • 코드의 각 Effect는 별도의 독립적인 동기화 프로세스를 나타내야 함. 
  • Effect는 반응형 값에만 반응한다.
    - 반응형 값? 컴포넌트 내부에서 선언된 props, state 및 기타 값은 렌더링 중에 계산되고 React 데이터 흐름에 참여하기 때문에 반응형임.
    - 반응형 값만 의존성 배열에 추가하면 된다.
  • 컴포넌트 본문에서 선언된 모든 변수는 반응형이다. 
    - props, state 뿐만아니라 이들로부터 계산하는 값들 역시 반응형 -> 의존성에 포함되어야 함 
    - 참고로, 변이 가능한 값(전역 변수 포함)은 반응형이 아님. 변이 가능한 값이 변경되더라도 컴포넌트가 다시 렌더링되지 않고, 렌더링 도중 변경 가능한 데이터를 읽는 것은 렌더링의 순수성을 깨뜨리기 때문에 React의 규칙을 위반함. 대신 useSyncExternalStore을 사용하여 외부 변경 가능한 값을 읽고 구독해야 함. ex) ref.current, location.pathname 

재동기화를 원치 않는 경우에 어떻게 해야 하나? 

  • 렌더링에 의존하지 않고 항상 같은 값을 갖는다면 컴포넌트 외부로 값을 옮길 수 있음.
  • Effect 내부에서 값을 선언할 수 있음. 렌더링 중에 계산되지 않으므로 반응하지 않음. 

Effect는 반응형 코드 블록. 내부에서 읽은 값이 변경되면 다시 동기화된다. 상호작용당 한 번만 실행되는 이벤트 핸들러와 달리 Effect는 동기화가 필요할 때마다 실행된다.

 

6. Seperating Events  from Effects 

이벤트 핸들러와 Effect의 차이 

  • 이벤트 핸들러는 특정 상호 작용에 대한 응답으로 실행된다. 
  • Effect는 동기화가 필요할 때마다 실행된다. 

Effect에서 비반응형 로직을 추출하기 위해 Effect Event 선언하기 

useEffectEvent라는 특수 Hook을 사용한다. 

 

onConnect는 Effect Event라고 불리며 Effect 로직의 일부지만 이벤트 핸들러처럼 동작한다. 그 내부의 로직은 반응형으로 동작하지 않으며 항상 props와 state의 최신 값을 확인함. 

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]); // ✅ All dependencies declared
  // ...
  • Effect Event는 반응형 이벤트가 아니므로 의존성에서 생략해야 한다.  
  • Effect Event를 사용하면 억제하고 싶을 수 있는 많은 의존성 linter 패턴을 수정할 수 있다.
  • Effect 내부에서만 호출할 수 있음.
  • 다른 컴포넌트나 Hook에 전달해서는 안된다. 

7. Removing Effect Dependencies 

  • 의존성은 코드와 일치해야 한다. 
  • 의존성을 제거하려면 의존성이 아님을 증명해야 한다. 
  • 의존성을 변경하려면 코드를 변경해야 한다. 
  • 불필요한 의존성을 제거해야 한다. 
  • Effect가 아닌 이벤트 핸들러로 옮길 수 있는 것인지 고민해봐야 한다. -> 특정상호작용에 대한 응답으로 일부 코드가 실행되어야 하는 경우 해당 코드를 이벤트 핸들러로 이동해야 함 
  • Effect가 서로 관련이 없는 여러 가지 작업을 수행하고 있는지 확인해보아야 한다. (서로 관련이 없는 두가지를 동기화하고 있는지 확인해보아야 함) -> 여러 개의 Effect로 분할해야 함 
  • 이전 state를 기반으로 일부 state를 업데이트하려면 업데이터 함수를 전달해야 함 
  • 반응하지 않고 최신 값을 읽으려면 Effect에서 Effect Event를 추출할 수 있음 
  • 객체와 함수의 의존성을 피해야 함. 컴포넌트 외부나 Effect 내부로 이동시킬 것. (JavaScript에서 객체와 함수는 서로 다른 시간에 생성된 경우 서로 다른 것으로 간주되기 때문)

의존성 린터 억제가 위험한 이유? 

  • 매우 직관적이지 않은 버그가 발생하여 찾아서 수정하기가 어려움 

8. Reusing Logic with Custom Hooks

  • 커스텀 훅을 사용하면 컴포넌트 간에 로직을 공유할 수 있다.
  • 커스텀 훅의 이름은 use로 시작하고 대문자로 끝나야 한다.
    • 함수가 내부에 하나 이상의 훅을 사용하는 경우 함수에 use 접두사를 지정해야 함. 
  • 커스텀 훅은 상태적 로직만 공유하며 state 자체는 공유하지 않는다.
    • 각 훅 호출은 동일한 훅에 대한 다른 호출과 완전히 독립적이다. 
  • 반응형 값을 한 훅에서 다른 훅으로 전달할 수 있으며 최신 state로 유지된다.
    • 컴포넌트를 렌더링할 때마다 커스텀 훅 내부의 코드가 다시 실행된다. 이것이 컴포넌트와 마찬가지로 커스텀 훅도 순수해야 하는 이유. 
  • 커스텀 훅이 수신한 이벤트 핸들러를 Effect Event로 감싸라. -> 의존성 배열에서 제거할 수 있음 
  • 커스텀 훅의 이름은 코드를 자주 작성하지 않는 사람이라도 커스텀 훅이 무엇을 하고 무엇을 취하고 무엇을 반환하는지 짐작할 수 있을 정도로 명확해야 함. ex) useData(url), useImpressingLog(eventName, extraData
  • useMount와 같은 생명주기 커스텀 훅을 만들지 말 것. 용도를 명확히 해야 한다.
    • useMount와 같은 생명주기 훅은 React 패러다임에 잘 맞지 않음 

 

커스텀 훅으로 Effect를 감싸는 것의 장점 

  1. Effect와의 데이터 흐름을 매우 명확하게 만들 수 있다.
  2. 컴포넌트가 Effect의 정확한 구현보다는 의도에 집중할 수 있다.
  3. React가 새로운 기능을 추가할 때 컴포넌트를 변경하지 않고도 해당 Effect를 제거할 수 있다.

 

 

 

이 외에 알아 둘 것! 

컴포넌트 밖에서 전역변수를 선언해서 사용해도 렌더링 촉발하지 않는데, ref를 사용하는 이유? 

-> 다른 컴포넌트에서 해당 변수를 참조하지 않도록 하기 위해