상태와 리렌더링

리액트의 컴포넌트는 상태가 변하면 해당 컴포넌트를 재평가(함수형 컴포넌트라면 함수를 재실행, 클래스형 컴포넌트라면 다시 인스턴스화)하여 새로운 내용이 반영하여 새로 UI를 그립니다. 이러한 행위를 리렌더링(re-rendering)이라고 합니다. 결국 리액트는 최초 렌더링 - 상태의 변화 - 리렌더링(렌더링)의 단계를 거치며 새로운 컨텐츠를 화면에 표시하는 것이죠. 하지만 이렇게만 생각하면 조금 헷갈릴 수 있습니다. 혼란을 줄이기 위해 조금 더 상세한 단계를 알아봅시다.

출처: draw.io에서 직접 그림


먼저 리액트의 컴포넌트가 최초로 렌더링됩니다. 그리고 상태변화 요청이 오기 전까지는 계속 대기하죠. 그러다가 사용자가 버튼을 누르는 등의 행동을 통해 상태를 변화(set함수를 호출하는 로직을 실행)시키면 리액트는 해당 요청을 받아 새로운 값으로 상태를 변경합니다. 컴포넌트의 최초 변경과 상태(state)변화를 일으키는 행위를 트리거(Trigger)라고 부릅니다. 이 과정 중에 컴포넌트는 재평가가 됩니다(아직 다시 렌더링이 되지는 않았습니다.). 컴포넌트가 재평가되면 변화된 내용은 먼저 가상 DOM에 반영됩니다. 리액트에서는 이 과정을 렌더링(Rendering)이라고 합니다.

 

그러고나면 리액트는 가상 DOM과 실제 DOM의 내용을 비교합니다. 만약 두 내용이 다르다면 가상 DOM의 내용 중 실제 DOM과 다른 부분을 모두 긁어 모아 실제 DOM으로 내용을 한번에  덮어쓰게됩니다. 만약 실제 DOM에 있던 내용이 가상 DOM에는 없으면 그 내용은 삭제하게 되는 식이죠. 그러면 사용자는 리액트에 의해 변화된 콘텐츠를 볼 수 있게 됩니다. 이 과정을 커밋(Commit)이라고 하죠. 이때, 컴포넌트 재평가 후 가상 DOM과 실제 DOM의 내용이 같다면 리액트는 아무런 행동도 하지 않습니다. 해야될 일이 없거든요.

리액트는 가상 DOM과 실제 DOM의 서로 다른 내용을 컴포넌트 별로 분리해서 업데이트 하지 않습니다. 다른 내용을 모두 한 번에 실제 DOM에 반영하죠. DOM 조작은 시간이 걸리는 일(레이아웃을 계산하고 화면을 그리는 일은 리소스의 소모가 있고 동기적으로 반영되기 때문에 병목현상이 발생할 수 있습니다.)이기 때문에 한 번의 DOM 조작으로 모든 업데이트를 처리함으로써 더 빠른 처리를 할 수 있습니다.

 

출처: React.dev  렌더링 그리고 커밋

물론 이 과정은 정말 빠르게 일어나기 때문에 우리의 눈으로는 그 과정에서 일어나는 일을 구분하기란 불가능할지도 모릅니다. 하지만 useEffect Hook을 사용한다면 그 과정 중에 일어나는 일을 직접 확인할 수 있고 그 사이에 특정한 로직을 실행할 수도 있습니다.

useEffect

useEffect(setup, dependencies?)

useEffect는 컴포넌트의 생명주기와 비슷한데요. 하지만 react 공식 문서에서는 이와는 분명히 다르다고 언급하고 있습니다. 컴포넌트의 마운트/언마운트 등과는 실행 시점이 다를 수 있기 때문입니다. 정확하게는 생명주기보다는 우리가 지정한 props와 state에 따라 DOM과 동기화하는 시점이라고 보시면됩니다. 혹은 화면에 렌더링이 반영될 때까지 코드 실행을 '지연'시킨다고 생각하셔도 좋습니다.

 

실제 useEffect를 사용하는 상황은 컴포넌트의 부수효과(side effect)를 관리하기 위해서입니다. 이벤트 핸들러가 아닌 렌더링과 관련되어서(정확하게는 렌더링 이후) 실행되어야 하는 effect들이 있습니다. 예를 들어, 실시간 웹 소켓에 연결하여 실시간 채팅을 처리하는 경우가 그런데, 이러한 연결은 컴포넌트의 표시를 제어하는 상호 작용과 독립적으로 발생해야 합니다. useEffect를 사용한다면, 해당 함수는 렌더링 이후 실행되므로 컴포넌트 업데이트가 완료된 후 (백엔드 서버의 데이터를 받아오는 등)외부 시스템과 동기화 작업을 수행할 수 있습니다.

비어있는 의존성 배열을 받은 useEffect

먼저 컴포넌트의 생명주기와 동일한 시점에 실행되는 useEffect를 알아보겠습니다. 가장 직관적으로 이해하기 쉽습니다.

const SomeComponent = () => {
    useEffect(()=> {
        // 컴포넌트가 마운트 되었을 때 실행할 코드 : effect logic
        return () => {
            // 컴포넌트가 언마운트 되기 직전에 실행할 코드 : clean up logic or clean up function
        }
    }, []);
}

useEffect는 두 개의 인자를 받습니다. 첫번째 인자에는 함수, 두 번째 인자에는 배열이 들어가게 됩니다. 여기에 들어가는 함수는 useEffect가 실행하는 코드입니다. 그리고 useEffect가 실행되는 시점은 두 번째 인자인 배열에 달렸죠. 이 배열을 의존성 배열이라고 하는데, 현재는 빈 배열이 들어간 코드입니다.

의존성 배열
useEffect를 비롯한 몇몇 Hook들은 의존성 배열을 인자로 받습니다. 의존성 배열이라는 이름처럼 이 배열에는 해당 Hook이 의존하고 있는 값들이 들어가야 합니다. 기본적으로 이 의존성 배열 요소 값들이 바뀌면 Hook이 실행된다고 생각하면 됩니다. useEffect의 경우는 이 의존성 배열 내부의 값들이 변화하면 자신이 받은 함수를 실행하게 됩니다.

의존성 배열이 비어있기 때문에 해당 코드의 useEffect는 컴포넌트의 변화가 얼마나 많던지 상관하지 않습니다. 그래서 useEffect는 컴포넌트가 처음 마운트되었을 때 자신이 첫 번째 인자로 받은 함수인 effect 로직을 실행합니다. 그리고 이 함수가 반환하는 함수인 clean up 함수는 잘 가지고 있다가 컴포넌트가 언마운트될 때(현재 코드에선 이 단계가 리렌더링 직전 시점입니다.) 실행되도록 합니다.

의존성 배열을 받은 useEffect

그렇다면 의존성 배열에 요소가 있다면 어떻게 될까요?

const SomeComponent = () => {
    const [count, setCount] = useState(0)

    useEffect(()=> {
        // 첫 번째 렌더링 혹은 count가 변경되었을 때 되었을 때 실행할 코드
        return // count의 변경으로 다음 effect function이 호출되기 전 혹은 컴포넌트 언마운트 전에 실행할 코드
    }, [count]);
}

위의 코드에서 만약 count의 값이 변경된다면 그 즉시 useEffect의 effect 로직이 실행됩니다. 그리고 상태가 변경되었으니 컴포넌트를 재평가하게 되죠. 리렌더링 직전에 clean up 함수가 실행됩니다. 그런데 리액트의 리렌더링 방법에 따라, 컴포넌트 재평가 전과 후의 값이 같다면 clean up 함수는 실행되지 않습니다(물론 대부분의 경우에는 상태가 이전과는 다를 테니 clean up함수가 실행되는 경우가 많습니다.)! clean up 함수는 재평가 이후 값이 달라져, 리렌더링이 일어 나야하면 리렌더링 직전에 수행되는 것이죠.

의존성 배열을 받지 않은 useEffect

그런데 의존성 배열을 파라미터로 가지는 Hook들은 이 의존성 배열을 생략할 수 있습니다. 그렇게 되면 컴포넌트의 모든 렌더링마다 해당 Hook이 실행되죠.

const SomeComponent = () => {
    useEffect(()=> {
        // 매 렌더링마다 실행할 코드
        return // count의 변경으로 다음 effect function이 호출되기 전과 언마운트 직전에 실행할 코드
    });
}

위의 코드는 SomeComponent가 렌더링 될 때마다 실행되는 코드와 매 렌더링 직전(그리고 언마운트 직전)마다 실행할 코드를 작성할 수 있습니다.

clean up 함수의 추가 설명

만약 useEffect를 처음 접한다면? - clean up 함수 작성은 콜백함수처럼!

clean up 함수가 원하는 시점에 적용되도록 하기 위해서는 return 값에는 함수 정의문이 들어가거나 함수의 포인터 혹은 참조 값이 들어가야 함니다. 만약 함수를 실행 혹은 함수를 호출해버리면 effect logic과 같은 시점에 실행이 되어 버립니다. 이전에 언급했듯이 useEffect의 effect함수가 반환하는 값은 '잘 가지고 있다가' 리렌더링 혹은 언마운트 직전에 실행시킬 수 있어야 합니다. 그러기 위해서는 함수를 실행시키면 안되겠죠?

 

 Clean-up함수의 정확한 실행 시점을 알고 싶다면?

리액트의 컴포넌트가 반환하는 JSX코드가 가상 DOM에 반영됩니다. 그리고 이 작업은 이후에 설명할 useEffect의 effect(혹은 set-up)함수 및 clean-up 함수와 비동기적으로 실행됩니다.일반적으로 컴포넌트의 JSX코드의 반환이 먼저 실행되지만 모든 상황에서 JSX코드가 먼저 실행되는 것은 아니므로 컴포넌트의 JSX코드 반환과 useEffect의 함수 실행이 항상 같은 순서로 완료되지 않기 때문에 clean-up함수가 리렌더링 직전에 수행된다기 보다는 effect 함수의 재실행 전에 실행된다고 생각하시는 게 덜 혼란스러울 것입니다. 

참고 자료

react.dev [렌더링 그리고 커밋, useEffect, 반응형 effects의 생명주기 ]

[번역] useEffect 완벽 가이드

+ Recent posts