React Learn - Reactive Effect 의 LifeCycle

ChoiYongHyeun·2024년 3월 3일
1

리액트

목록 보기
14/31

이전 게시글들에서

??? : 웬만하면 useEffect 사용하지 마세요

라고 하는 글들을 포스팅 했지만 가끔씩 useEffect 를 사용해야 하는 일들이 잦고

useEffect 를 사용하지 않기 위해 사용하는 다른 라이브러리들도 useEffect 를 기반으로 개발된게 많기 때문에

useEffect 를 잘 이해하는 것이 중요하기에 공식문서를 보고 정리해보도록 한다.

이번 게시글에서는 useEffect 관점에서 리액트 컴포넌트를 바라보는 것에 대해 중점으로 한다.

Lifecycle of Reactive Effects


EffectLifeCycle

컴포넌트의 라이프 사이클

리액트 컴포넌트는 3가지 라이프 사이클을 갖는다.

  1. 컴포넌트가 새롭게 스크린에 mount
  2. 컴포넌트의 props , state 등이 인터렉션에 의해 변경되어 update
  3. 컴포넌트가 스크린에서 unmount

여기서 우리는 useEffect 의 라이플 사이클을 컴포넌트 라이프 사이클에 맞춰 이해하게 되면

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId]);
  // ...
}

다음과 같은 코드가 있을 때 , useEffect 내부의 콜백 함수는 mount , update 될 때 실행되고

콜백함수의 반환값은 unmount 될 때 실행되는구나 ~

이렇게 이해 할 수 있다.

그리고 deps 배열은 useEffect 가 호출될 조건인 의존성을 갖는 값이구나 ~ 이렇게 이해 할 수 있다.

하지만 이보다 useEffect 를 잘 이해하기 위해서는 useEffect 만의 라이프 사이클을 이해하는 것이 좋다.

useEffect 의 라이프 사이클

useEffect 는 컴포넌트가 external systemsynchronize 하기 위해 존재한다고 하였다.

Commit 단계 이후 실행되게 함으로서 컴포넌트의 상태를 항상 Commit 이후 발생하는 값들과 동기화가 가능하다.

그럼 useEffect 의 라이프 사이클을 synchronize 를 기준으로 하여 생각해보자

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
	// 1. Start Synchronize  
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
	// 2. Stop Synchronize 
    return () => {
      connection.disconnect();
    };
  }, [roomId]);
  // ...
}

위 코드의 useEffect 의 콜백함수는 컴포넌트가 렌더링 된 후 external system 과 동기화 할 때 실행되고

콜백함수의 반환 함수는 external system 과 동기화를 그만 할 때 실행된다고 생각 할 수 있다.

이 때 동기화를 그만 하는 주체는 1 에서 동기화를 시작했던 어떤 reactive 한 값일 때의 컴포넌트이다.


useEffect 만의 LifeCycle 로 이해하면 좋은 점

컴포넌트들은 한 번 마운트 된 이후로 인터렉션에 따라 여러번 업데이트 되기도 하고

디마운트, 업데이트 된다.

이럴 때 마다 useEffect 를 바라 볼 때 mount ... unmount ... update 의 관점으로 바라보기 보다

어떤 reactive 한 값에 따라 컴포넌트의 시점을 나누고

시점 별 컴포넌트의 동기화로 생각해보면 훨씬 더 useEffect 의 목적에 맞게 직관적으로 이해하는 것이 가능하다.

function ChatRoom({ roomId }) { // The roomId prop may change over time
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // This Effect reads roomId 
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // So you tell React that this Effect "depends on" roomId
  // ...

다음과 같이 roomId 에 의존하고 있는 useEffect 훅을 보았을 때

props 로 내려오는 roomId 에 따라 useEffect hookmount , uupdate , unmount 될 때 어떻게 실행된다 라기보다

useEffect 는 항상 컴포넌트와 external system 을 동기화 해두는데 roomId 값에 따라

동기화를 시작하고 끊는다 (새로운 값이 들어오면 다시 동기화를 시작한다.)

라고 이해하면 훨씬 직관적이다.

이렇게 useEffectdeps 배열에 reactive 한 값을 넣음으로서

동기화를 시작하고, 동기화를 끝맺을 조건을 매긴다.


useEffectreactive 한 값에 반응한다

useEffect 를 사용 할 때 두 번째 인수인 deps 배열 내에는 reactive value 를 넣어줘야 한다.

만약 컴포넌트 내부에 reactive value 가 존재하는데 값을 넣어주지 않으면

리액트의 lint 가 오류를 발생 시킨다.

reactive value 의 값이 변경되었음에도 useEffect 가 변경된 값 이전의 결과를 계속 가져와 생기는 괴리를 방지하기 위함이다.

이전 reactive 한 값에 반응한다고 하였다.

컴포넌트의 reactive 한 값이란 단순히 mutable 한 값이 아닌

렌더링 동안 변경이 가능한 값 을 의미한다.

렌더링 동안 변경이 가능한 값이란 무엇일까 ?

props

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId]);
  // ...
}

위 예시에서 roomId propsChatRoom 컴포넌트가 렌더링 될 때 (호출된 경우) 값이 변경 가능한 값이다.

새로운 roomId 값을 받을 수 있기 때문이다.

컴포넌트 외부에 존재하는 serverUrl 변수는 컴포넌트 외부에서 어떻게 변경 될 수 있을지 몰라도

ChatRoom 컴포넌트가 호출되어 렌더링 되는 동안에는 값이 변경 될 수 없기에 reactive 한 값이 아니다.

state

function ChatRoom({ roomId }) { // Props change over time
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); 
  // State may change over time

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); 
    // Your Effect reads props and state
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]);
  // So you tell React that this Effect "depends on" on props and state
  // ...
}

이번에는 컴포넌트 내부에서 정의된 state 의 경우 동일한 serverUrl 이라 할지라도

렌더링 되는 동안 변경이 가능하기 때문에 이는 reactive 한 값이다.

이는 setServerUrl 함수를 사용하지 않아 serverUrl 의 값을 절~대 변경 시키지 않는다고 하더라도
컴포넌트 내부에서 정의된 serverUrl statereactive value 로 정의된다.

conditional value

function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
  const settings = useContext(SettingsContext); // settings is reactive
  const serverUrl = selectedServerUrl ?? settings.defaultServerUrl;
  // serverUrl is reactive
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); 
    // Your Effect reads roomId and serverUrl
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]); 
  // So it needs to re-synchronize when either of them changes!
  // ...
}

위 예시에서 settings 는 상위 컴포넌트에서 정의된 context 값에 따라 값이 변하는

reactive value 이다.

마찬가지로 serverUrlsettings 에 따라 값이 변하는 reactive value 이다.

이 때 useEffect 관점에서 동기화를 할 조건으로 settings , serverUrl 중 어느 것을 사용해도 동기화가 가능하지만

serverUrl 처럼 렌더링의 결과와 직접적으로 관련있는 값을 선택하는 것이

불필요한 useEffect 의 호출을 막을 수 있다.

All values from the component body

All values inside the component (including props, state, and variables in your component’s body) are reactive. Any reactive value can change on a re-render, so you need to include reactive values as Effect’s dependencies.

In other words, Effects “react” to all values from the component body.

리액트 공식문서에서는 컴포넌트 내부에 정의된 모든 변수들 또한 reactive value 로 정의 할 수 있다고 한다.

그 중 이전 상태와 다른 re-render 를 일으키는 값은 무조건 deps 배열에 넣어줘야 한다고 한다.

Global valuereactive values 일까 ?

그럼 컴포넌트 외부에서만 정의된 mutable 한 값은 reactive value 로 볼 수 있을까 ?

예를 들어 컴포넌트 외부에서 정의된 stateref.current 값과 같이 말이다.

Mutable values (including global variables) aren’t reactive.
??? : 아닙니다

reactive value 로 볼 수 없는 이유는

컴포넌트 외부에서만 정의된 mutable value 의 변화가 컴포넌트 렌더링에 영향을 미치지 않기 때문이다.

만약 외부에서 정의된 mutable valueprops 로 전달받는다면 이는 당연하게 reactive value 로 정의된다.

하지만 외부에서 정의된 mutable value 는 값이 변화되더라도

해당 컴포넌트의 re-render 를 야기하지 않기 때문에

useEffect 의 동기화 기준이 될 수 없다.

만약 동기화 시키고 싶다면 내부에서 불러오든지 useSyncExternalStore 를 이용해서 불러오도록 하자

useRef 는 조금 특별한 이유로 reactive value 라고 볼 수 없다.

컴포넌트 내부에서 useRef 로 이전에 생성한 ref 객체를 불러와 사용한다고 해보자
이 때 refmutable 한 값임은 모두가 알고 있다.

ref 객체가 다른 곳에서 변경이 되어 컴포넌트의 렌더링 로직이 변경될 수 있기 때문에
reactive value 로 생각 할 수도 있지만
리액트에서 값이 같느냐 다르느냐를 확인하는 기준은 저장된 메모리 주소에 기인한다.

ref 객체는 mutable 하더라도 참조하고 있는 메모리 주소는 항상 동일하기 때문에 ref 객체 자체는 값이 다르더라도 reactive value 라고 할 수 없다.

또한 re-render 를 야기하지 않는 값이기 때문에 더더욱 그렇다.

ref.currentactual DOM 을 지정해둔 경우 ref.current 또한 deps 배열로 볼 수 없다.
그 이유는 다음과 같다.

  1. currentactual DOM 에 값을 지정해두고, 컴포넌트 외부에서 직접 actual DOM 의 값을 변경하는 경우 리액트에 의해 일어난 것이 아니기 떄문에 리액트 데이터 플로우와 연관이 없다.

  2. actual DOM 의 변화는 react 에서 감지하지 못하기 때문에 reactive value 라고 할 수 없다.

deps 배열을 [] 로 두는 경우에 대한 이해

이전 글에서 deps 배열을 정의하지 않는 경우와 , deps 배열을 [] 처럼 비워두는 경우

다른 결과를 보인다고 하였다.

배열을 비워두는 경우 컴포넌트 라이프 사이클 관점에서는

마운트 , 언마운트 될 때만 useEffect 훅을 작동시킨다

하지만 이렇게 이해하기보다 useEffect 의 라이프 사이클 관점에서 이해한다면

동기화 할 조건이 없기 때문에 동기화를 한 번만 시행하자라고 이해 할 수 있다.


회고

컴포넌트 내부에 존재하는 reactive value 는 모두 useEffectdeps 배열에 들어가야 하는구나

다음 챕터에서는 reactive valuedeps 넣지 않아도 되는 useEffectEvent 에 대한 공부를 할 예정이다.
지금 읽고 있는데 잘 이해는 안된다

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글