React는 UI를 변경된 부분만 업데이트하여 빠른 사용자 경험을 제공하는 것으로 알려져 있습니다.
React의 렌더링 성능을 살펴보면 이해하기 어려운 몇 가지 용어와 개념이 있습니다. 한동안 VDOM이 무엇인지, React가 컴포넌트를 리렌더링 하는 방법을 100% 알 수 없었습니다.
이 글의 첫 번째 부분에서는 React의 렌더링에 대한 가장 중요한 개념과 React가 특정 컴포넌트의 리렌더링을 결정하는 방법을 설명합니다.
이 글의 마지막 부분에서는 React 애플리케이션의 렌더링 성능을 최적화하기 위해 수행할 수 있는 작업을 보여 줍니다.
이 글을 읽으신 후, 궁금한 점이 있거나 잘못된 부분이 있다면 언제든지 댓글을 남기거나 이메일을 보내주세요.
React가 어떻게 렌더링하고 리렌더링 하는지 알고 싶으시다면 라이브러리 뒤에서 무슨 일이 일어나는지 이해하는 것이 좋습니다.
렌더링은 다양한 추상화 수준에서 이해할 수 있는 용어입니다. 문맥에 따라 약간 의미가 다르지만, 궁극적으로는 이미지를 생성하는 과정을 설명합니다.
시작하려면 DOM(Document Object Model)이 무엇인지 이해해야 합니다.
"W3C Document Object Model(DOM)은 프로그램 및 스크립트가 동적으로 접근하여 문서의 스타일과 구조 및 콘텐츠를 업데이트할 수 있는 언어 중립적 인터페이스입니다."
쉽게 말해서 DOM은 웹 사이트를 열 때, 화면에 표시되는 내용을 마크업 언어인 HTML을 통해 표현한다는 것을 의미합니다.
브라우저에서는 자바스크립트 API를 통해 DOM을 수정할 수 있습니다. 전역적으로 사용 가능한 document
는 HTML DOM의 해당 상태를 나타내며 수정 기능을 제공합니다.
또한, DOM 프로그래밍 인터페이스에 내장되어 있는 document.write
, Node.appendChild
또는 Element.setAttribute
와 같은 함수를 통해 자바스크립트로 DOM을 수정할 수 있습니다.
다음으로 React의 Virtual DOM(또는 VDOM)과 그 위에 또 다른 추상화 계층이 있습니다. 이것은 React 애플리케이션의 요소들로 구성됩니다.
애플리케이션의 상태 변경은 VDOM에 먼저 적용됩니다. VDOM의 새로운 상태에 대한 UI 변경이 필요한 경우, ReactDOM 라이브러리는 업데이트해야 할 항목 만 업데이트하여 효율적으로 작업을 수행할 수 있습니다.
예를 들어, 요소의 속성만 변경되는 경우, React는 document.setAttribute
(또는 이와 유사한) 호출을 통해 HTML 요소의 속성만 업데이트합니다.
빨간색 점은 DOM 트리의 업데이트를 나타냅니다.
VDOM을 업데이트한다고 해서 반드시 실제 DOM의 업데이트가 트리거 되는 것은 아닙니다.
VDOM이 업데이트되면, React는 이전의 VDOM 스냅샷과 비교한 뒤 실제 DOM에서 변경된 내용만 업데이트합니다. 아무것도 변경되지 않으면 전혀 업데이트되지 않습니다. 이전 VDOM과 새 VDOM을 비교하는 이 프로세스를 diffing이라고 합니다.
실제 DOM 업데이트는 UI를 다시 그리기 때문에 느립니다. React는 실제 DOM에서 가능한 가장 적은 범위를 업데이트하여 이를 더 효율적으로 만듭니다.
따라서 네이티브 DOM 업데이트와 가상 DOM 업데이트의 차이점을 인식할 필요가 있습니다.
이것이 어떻게 작동하는지에 대한 자세한 내용은 재조정(reconciliation) 에 대한 React 문서를 참조하십시오.
React에서 렌더링에 대해 이야기할 때, 우리는 대부분 render 함수의 실행에 대해서만 이야기합니다. 하지만 렌더링이 항상 UI 업데이트를 의미하지는 않습니다.
다음 예시를 살펴보겠습니다.
const App = () => {
const [message, setMessage] = React.useState('');
return (
<>
<Tile message={message} />
<Tile />
</>
);
};
함수형 컴포넌트에서 함수 전체를 실행하는 것은 클래스형 컴포넌트의 render 함수와 동일합니다.
부모 컴포넌트의 상태가 변경되었을 때(이 경우는 App
), 두 번째 컴포넌트는 props를 받지 않아도 2개의 Title
컴포넌트 모두 리렌더링이 됩니다.
이것은 render
함수가 3번 호출되는 것처럼 해석되지만, 실제 DOM 수정은 메시지를 표시하는 Title
컴포넌트에서 1번만 발생합니다.
빨간색 점은 리렌더링을 나타냅니다.
React에서 이것은 render 함수를 호출하는 것을 의미하며 실제 DOM에서 이것은 UI를 다시 그리는 것을 의미합니다
좋은 소식은 React는 이미 이것을 최적화하기 때문에 UI 다시 그리기의 성능 병목 현상에 대해 크게 걱정할 필요가 없다는 것입니다.
나쁜 소식은 왼쪽에 있는 모든 빨간색 점에 해당하는 컴포넌트들의 render 함수들이 실행되었다는 것입니다.
이러한 render 함수의 실행에는 두 가지 단점이 있습니다.
React는 그 차이를 매우 효율적으로 계산하기 때문에 첫 번째 단점은 그다지 중요하지 않습니다. 위험한 것은 사용자가 작성한 코드가 모든 React 렌더링에서 반복적으로 실행된다는 점입니다.
위의 예시는 작은 컴포넌트 트리였습니다. 하지만 각 노드에 더 많은 자식들이 있고 이 안에도 하위 컴포넌트가 있을 수 있을 때, 어떤 일이 발생하는지 상상해 보세요. 이제 우리는 이런 상황을 어떻게 최적화할 수 있는지 방법을 살펴보겠습니다.
React DevTools를 사용하면 Components 탭 -> 환경 설정 아이콘 -> Highlight updates when components render
를 통해 가상 렌더링을 강조 표시할 수 있습니다.
네이티브의 리렌더링을 보려면 Chrome DevTools에서 오른쪽의 점 3개 메뉴 -> More tools -> Rendering -> Paint flashing
를 통해 볼 수 있습니다.
이제 React 리렌더링이 강조 표시된 애플리케이션을 클릭한 다음, 네이티브 렌더링을 클릭하면 React가 네이티브 렌더링을 얼마나 최적화하는지 확인할 수 있습니다.
작동 중인 Chrome 페인트 깜빡임 옵션 예시
위에서 UI를 다시 그리는 원인을 보았습니다. 하지만 처음에 React의 render 함수를 호출하는 것은 무엇일까요?
React는 컴포넌트의 상태가 변경될 때마다 렌더링을 예약합니다.
렌더링 예약은 이 작업이 즉시 수행되지 않는다는 것을 의미합니다. React는 이에 가장 적합한 순간을 찾기 위해 노력할 것입니다.
상태를 변경한다는 것은 setState
함수(React hooks에서는 useState
)를 실행할 때, React 트리거가 업데이트된다는 것을 의미합니다. 이는 컴포넌트의 render 함수가 호출된다는 것을 의미할 뿐만 아니라, props 변경 여부와 관계없이 모든 하위 컴포넌트들이 리렌더링 된다는 것을 의미합니다.
애플리케이션이 제대로 구조화되어 있지 않은 경우, 상위 노드를 업데이트하면 모든 자식들의 render
함수를 실행해야 하기 때문에 예상보다 훨씬 더 많은 자바스크립트를 실행하게 될 수 있습니다.
이 글의 마지막 부분에서는 이러한 종류의 오버헤드를 방지하는 데 도움이 되는 몇 가지 팁을 소개합니다.
props가 변경되었음에도 React가 구성 요소를 업데이트하지 않는 2가지의 일반적인 이유가 있습니다.
setState
를 통해 올바르게 업데이트되지 않았습니다.앞서 살펴본 바와 같이, React는 setState
함수(또는 함수형 컴포넌트에서는 useState
hook)로 상태를 변경했을 때, 컴포넌트를 리렌더링 합니다.
결과적으로 하위 컴포넌트는 상위 컴포넌트의 상태가 해당 함수 중 하나로 변경될 때만 업데이트됩니다.
props 객체를 직접 변경하는 것은 변경 사항을 트리거하지 않으며 React가 변경 사항을 인식하지 못하기 때문에 허용되지 않습니다.
this.props.user.name = 'Felix';
이렇게 하지 마세요!
이렇게 props를 변경하는 대신, 부모 컴포넌트의 상태를 변경해야 합니다.
const Parent = () => {
const [user, setUser] = React.useState({ name: 'Felix' });
const handleInput = (e) => {
e.preventDefault();
setUser({
...user,
name: e.target.value,
});
};
return (
<>
<input onChange={handleInput} value={user.name} />
<Child user={user} />
</>
);
};
const Child = ({ user }) => (
<h1>{user.name}</h1>
);
해당 React 함수로 상태를 변경하는 것이 중요합니다. Codepen에서 확인할 수 있습니다.
상태 업데이트를 React.useState
로부터 받은 함수인 setUser
를 통해 하고 있다는 것을 유의하세요. 이것은 클래스형 컴포넌트에서 this.setState
와 동등합니다.
전문적으로 React를 이용하여 일해 온 2년 동안, 제가 강제 리렌더링이 필요한 순간은 한 번도 오지 않았습니다. 만약 당신에게 그 순간이 왔다면 업데이트되지 않은 React 컴포넌트들을 처리하는 더 좋은 보편적인 방법이 있기 때문에 이 글을 처음부터 읽기를 권장합니다.
하지만 반드시 강제 업데이트가 필요한 경우, 다음의 방법으로 강제 업데이트를 할 수 있습니다.
forceUpdate
함수 사용하기이것이 가장 확실한 방법입니다. React 클래스형 컴포넌트에서 다음 함수를 호출하여 강제로 리렌더링 할 수 있습니다.
this.forceUpdate();
React hooks에서는 forceUpdate
기능을 사용할 수 없지만 다음과 같이 컴포넌트 상태를 변경하지 않고 React.useState
를 통해 강제 업데이트를 할 수 있습니다
const [state, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
StackOverflow를 참고해 작성했지만, 아마 이 코드가 필요하진 않을 것입니다.
비효율적인 리렌더링의 예로는 상위 컴포넌트가 하위 컴포넌트의 상태를 제어하는 경우입니다. 기억하세요! 컴포넌트의 상태가 변경되면 모든 자식들이 리렌더링 됩니다.
React.memo 를 설명하기 위해 기존의 예제를 확장하여 더 많은 중첩된 자식을 이용하였습니다. 한번 사용해보세요.
노란색으로 표시된 숫자는 각 컴포넌트의 render
함수가 실행된 횟수입니다.
codepen에서 직접 실행해보세요.
파란색 컴포넌트의 상태만 업데이트했지만 다른 컴포넌트의 렌더링이 훨씬 더 많이 발생되었습니다.
React는 이러한 불필요한 업데이트를 방지하기 위해 몇 가지 함수를 제공합니다.
이 함수들을 먼저 살펴본 뒤, 렌더링 성능을 더 효과적으로 향상시킬 수 있는 방법들을 보여 드리겠습니다.
첫 번째는 아까 제시한 React.memo
입니다. 저는 이미 이것에 대해 더 심도 깊은 글을 작성하였지만, 요약하자면 React Hook 컴포넌트에서 props가 변경되지 않았을 때 렌더링 되는 것을 방지하는 함수입니다.
이 작업의 예시는 다음과 같습니다.
const TileMemo = React.memo(({ children }) => {
let updates = React.useRef(0);
return (
<div className="black-tile">
Memo
<Updates updates={updates.current++} />
{children}
</div>
);
});
프로덕션에서 사용하기 전에 이에 대해 알아야 할 몇 가지 사항이 더 있습니다. 이 글을 읽은 후, React.memo에 대한 제 글도 한 번 읽어보세요.
React 클래스에서 동등한 것은 React.PureComponent
입니다.
이 함수는 React의 라이프 사이클 함수 중 하나이며 클래스형 컴포넌트의 업데이트 시점을 React에게 알려줌으로써 렌더링 성능을 향상시킬 수 있습니다.
이것의 인자 값들은 컴포넌트가 렌더링 하려는 다음 props 및 다음 상태입니다.
shouldComponentUpdate(nextProps, nextState) {
// return true or false
}
이 함수의 사용은 매우 간단합니다. true
를 반환하면 React가 render 함수를 호출하고 false
를 반환하면 이를 방지합니다.
React에서는 다음을 수행하는 것이 매우 일반적입니다. 무엇이 문제인지 찾아보세요.
<div>
{
events.map(event =>
<Event event={event} />
)
}
</div>
key
속성을 설정하는 것을 잊었습니다. 대부분의 린터들은 이에 대해 경고하지만, 이것이 왜 중요할까요?
경우에 따라, React는 컴포넌트를 식별하고 성능을 최적화하기 위해 key
속성에 의존합니다.
위의 예시에서, 이벤트가 배열의 시작 부분에 추가되면 React는 첫 번째 원소와 이후 모든 원소들이 변경되었다 생각하고 해당 요소를 리렌더링 합니다. 요소에 key 속성을 추가함으로써 이를 방지할 수 있습니다.
<div>
{
events.map(event =>
<Event event={event} key={event.id} />
)
}
</div>
배열의 인덱스를 key로 사용하는 것을 피하고 내용을 식별하는 것을 key로 사용하세요.
key는 형제간에만 고유해야 합니다.
리렌더링을 개선하는 더 좋은 방법은 코드를 약간 재구성하는 것입니다.
로직을 어디에 두는지 주의해야 합니다. 애플리케이션의 루트 컴포넌트에 모든 것을 넣는다면, 내부에 있는 모든 React.memo
함수는 성능 문제를 해결하는 데에 도움이 되지 않습니다.
만약 데이터가 사용되는 곳에 더 가깝게 배치한다면, React.memo
가 필요하지 않을 수도 있습니다.
최적화된 버전의 예제를 확인하고 텍스트를 입력해보세요.
이 예시는 별도로 제공되지 않아 원문에서 직접 확인해보시길 추천 드립니다.
상태가 업데이트되더라도 다른 컴포넌트는 전혀 리렌더링 되지 않습니다.
유일하게 변경한 것은 상태를 처리하는 코드를 별도의 컴포넌트로 이동시킨 것뿐이었습니다:
const InputSelfHandling = () => {
const [text, setText] = React.useState('');
return (
<input
value={text}
placeholder="Write something"
onChange={(e) => setText(e.target.value)}
/>
);
};
애플리케이션의 다른 부분에서 상태를 사용해야 하는 경우, React Context 또는 MobX 및 Redux와 같은 대안을 사용하면 됩니다.
이 글 에서는 컴포넌트 구성과 성능 향상에 도움이 되는 방법에 대해 자세히 설명합니다.
React의 렌더링 메커니즘이 작동하는 방식과 이를 최대한 활용하기 위해 할 수 있는 일에 대해 더 잘 이해할 수 있기를 바랍니다. 이 글에서 저는 React의 렌더링 작동 방식을 더 잘 이해하기 위해 주제에 대한 추가 조사를 수행해야 했습니다.
저는 프런트엔드 성능에 대해 더 많은 글을 작성할 생각이므로 최신 글에 대한 알림을 받고 싶다면 Twitter에서 저를 팔로우하고 저의 이메일 목록을 구독해주세요.
여기까지 읽으셨다면, API에 대해 더 자세히 설명하고, 발생할 수 있는 몇 가지 일반적인 함정과 왜 React.memo를 항상 사용하면 안 되는지에 대한 이유를 설명하는 React.memo에 대한 글 을 확인하고 싶으실 것입니다. 읽어 주셔서 감사합니다.
🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article(https://kofearticle.substack.com/)을 구독해주세요!
I used to only look for popular or cool stuff, but reading this post makes me feel a lot.. Thanks! I hope to have more good articles in the future Bloxorz
Thanks for sharing this wonderful post. And it is informative too. I want to get more information like this article. Please keep sharing. mcdvoice
I really appreciate the information and advice you have shared with us. Many thanks for sharing this useful and valuable information.
myACPNY Portal
I really like the information you shared in the article. I hope you will post more interesting articles and information in the future. retro bowl
React provides a speedy experience by updating only the parts of the UI that have changed. Optimal features are always awaited and well received Five Nights at Freddy's 4
@stickman hook React speeds up the user experience by only updating the UI elements that have changed.
I don't see any errors. I think it's quite complete and complete. Elastic man