리액트의 작동원리, 자식 컴포넌트 재평가, react.memo()
리액트의 작동원리
우선 리액트는 사용자 인터페이스 구축을 위한 자바스크립트 라이브러리이다. 이 리액트의 핵심은 컴포넌트인데, 개발자는 사용자 인터페이스의 구축을 위해 컴포넌트를 사용하고, 리액트는 컴포넌트를 통해 사용자 인터페이스를 효과적으로 구성하며 이에 대한 업데이트 역시 컴포넌트를 통해서 하게 된다.
여기서 리액트 DOM에 대해 알아야하는데, 이는 웹에 대한 인터페이스이다. 그건 곧 리액트는 웹을 모른다는 것이다. 브라우저와 전혀 관계가 없는 것이다. 이 리액트는 컴포넌트를 어떻게 다룰지는 알지만 이런 컴포넌트에 HTML 요소들이 있는지, 다른 허구적인 요소들이 있는지 상관하지 않는다. 이는 HTML 요소들을 실제로 화면에 표시해주는 리액트 DOM이 상관하게 된다.
리액트는 컴포넌트를 관리하고 상태와 객체를 관리하고, 다른 객체의 상태와 컴포넌트가 바뀌어야 하는지 확인하고 컴포넌트의 변경 전후 상태를 확인하는 라이브러리일 뿐이다. 또, 리액트는 변경된 내용과 화면에 표시되어야 할 모든 요소를 리액트DOM 같은 현재 사용중인 인터페이스에 전달한다. 이 리액트DOM은 브라우저의 일부인 실제 DOM에 대한 작업을 하므로 사용자가 보고있는 화면에 무언가 표시하는 역할은 리액트DOM이 하는 것이다.
또, 리액트는 props를 관리하는데, props는 컴포넌트에 전달하는 데이터로 컴포넌트 구성을 가능하게 해주고, 부모와 자식 컴포넌트 간을 연결해준다. 리액트는 컴포넌트 내부 데이터인 상태(state)와 컴포넌트 전체의 데이터인 컨텍스트(context)도 다루게 된다. 리액트의 핵심 기능들을 다루는 것이다.
이 props, 상태 또는 컨텍스트가 변경되면 이를 사용하는 컴포넌트 역시 리액트를 통해 변경되고, 리액트는 이 컴포넌트가 화면에 새로운 것을 표시하는지 확인하게 된다. 화면에 뭔가 그려야 한다면 리액트는 리액트DOM에 이를 알려서 리액트DOM이 새 화면과 새 컴포넌트, 새 출력을 표시할 수 있게 해준다.
이렇게 컴포넌트와 실제 DOM과의 통신을 보면, 어떻게 통신이 되는가에 대한 궁금증이 생기게 된다. 최종적으로 리액트가 하는 역할은 가상DOM이란 개념을 사용하는 것이라 볼 수 있는데, 가상DOM은 앱이 마지막에 만들어내는 컴포넌트 트리를 결정하게 된다. 각각의 하위 트리를 가진 컴포넌트들은 JSX 코드로 반환되는데, 이 가상DOM은 컴포넌트 트리의 현재 모양과 최종 모양을 결정하게 된다.
정리하면, 상태나 속성의 변경으로 인해 컴포넌트 UI가 업데이트되어야할 때, 리액트는 가상DOM을 사용하여 최적의 업데이트 전략을 결정한다. 리액트는 실제 DOM과 가상 DOM을 비교하여 변경된 부분을 식별하고, 필요한 최소한의 변경만을 적용하게 된다.
위 이미지는 보라색 버튼을 누르면 This is new!라는 단락이 나오도록 한 예시인데, 위 콘솔 캡쳐사진을 보면 단락태그 p가 깜빡이는 걸 볼 수 있다. 이 부분만 변경되었다는 뜻이다. 이렇게 리액트는 가상DOM에서 변경된 부분만을 실제DOM에 적용한다. 이 과정에서 브라우저의 실제 DOM 조작은 최소화되기에 성능이 향상된다.
자식 컴포넌트 재평가
위에서 봤던 일련의 과정 중 자식 컴포넌트들은 재평가된다. 그래서 위에서처럼 버튼이 클릭되면 단락태그가 표출되도록 한 기능에 의해 재평가되면서 단락이 나타나는 것이다.
여기서 컴포넌트 재평가와 컴포넌트 내 함수의 재실행이 일어나도 실제 DOM이 다시 렌더링되거나 변경되지는 않는다. 분명히 이해해야 할 것은 컴포넌트가 재실행되면 이의 자식 컴포넌트들 역시 재실행되고 재평가된다는 것이다. 또한, 자식 컴포넌트가 또 다른 자식 컴포넌트를 가지고 있다면, 해당 자식 컴포넌트들도 재평가된다. 컴포넌트 트리의 아래로 내려가며 반복적으로 수행되는 것이다. 이러한 일련의 과정에서 리액트는 최적화를 위해 변경된 부분만을 업데이트하고, 필요한 경우에만 실제 DOM을 조작한다.
결과적으로 자식 컴포넌트의 재평가 과정은 리액트의 가상 DOM과 조화를 이루게 되고, 효율적인 UI 업데이트를 가능하게 한다. 또, 변경된 부분만을 업데이트하기에 불필요한 자원낭비가 최소화되어 웹앱의 성능도 향상시킬 수 있다.
불필요한 재평가 방지
재평가를 계속 거치다보면 어딘가에 불필요하게 재평가가 반복되는 부분이 있을 수 있다. 이 경우 해당 컴포넌트의 재평가를 방지해줄 필요가 있는데, 이런 경우 react.memo() 메서드를 사용하게 된다.
위 코드는 한 자식 컴포넌트의 코드인데, 자식 컴포넌트가 재평가될 때마다 콘솔창에 hello를 뜨게 하고, 부모 컴포넌트의 버튼이 클릭되면 이 컴포넌트의 단락이 표출된다. 그리고 예시를 위해 이 컴포넌트의 export 구문에 React.memo를 사용하고 괄호 안에 자식 컴포넌트의 이름을 넣었다.
그랬더니 그 이후부터는 최초 한번만 hello가 표출되고, 이후에는 App 컴포넌트의 APP RUNNING 문구만 계속 표출되고 자식 컴포넌트의 hello 문구는 표출되지 않았다. 자식 컴포넌트는 더 이상 재평가되지 않은 것이다.
memo는 함수형 컴포넌트에서만 사용 가능하며, 클래스 컴포넌트에서는 사용할 수 없다.
memo는 인자로 들어간 컴포넌트에 어떤 props가 입력되는지 확인하고 입력되는 모든 props의 새로운 값을 확인한 뒤 이를 기존의 props 값과 비교하도록 리액트에 전달한다. 그리고 props 값이 바뀐 경우에만 컴포넌트를 재평가·재실행하게 된다. 그리고 부모 컴포넌트가 변경되었지만 그 컴포넌트의 props 값이 바뀌지 않았다면 컴포넌트 실행은 건너뛰게 된다. 만약 재실행되지 않는 자식 컴포넌트에 다른 자식 컴포넌트가 있다면, 그 컴포넌트 역시 재실행되지 않는다. memo는 이런 기능을 함으로써 불필요한 재렌더링을 막고 최적화를 이루는데 도움이 된다.
기능만 보면 최적화작업을 할 때 항상 사용할 것 같지만, 최적화에는 비용이 따르기 때문에 항상 사용하지는 않는다.
그래서 props가 변경되었을 때만 컴포넌트가 렌더링되는 경우나 컴포넌트가 복잡한 계산이나 데이터를 처리하는 경우, props에만 의존하고 내부의 상태나 부수 효과가 없는 순수 함수 컴포넌트 같은 경우에는 memo 메서드를 이용하여 성능 향상에 도움을 줄 수 있다.
그런데 컴포넌트가 너무 자주 변경되거나 속성 값이 객체나 배열과 같은 참조 타입인데 자주 바뀌는 경우에는 사용하지 않는 것이 좋다. 특히 속성 값이 객체나 배열이면 내부의 값이 변경되지 않아도 변경이 감지되지 않을 수가 있다. 그래서 이런 경우엔 memo보다는 속성 값이 변경될 때마다 렌더링되도록 하는 것이 좋다.
