React의 렌더링 동작 이해하기
게시 됨: 2020-11-16삶, 죽음, 운명, 세금과 함께 React의 렌더링 동작은 인생에서 가장 큰 진실이자 신비 중 하나입니다.
뛰어들자!
다른 사람들과 마찬가지로 저는 jQuery로 프론트엔드 개발 여정을 시작했습니다. 순수한 JS 기반 DOM 조작은 그 당시에는 악몽이었으므로 모두가 하던 일이었습니다. 그러다 서서히 자바스크립트 기반 프레임워크가 너무 유명해져서 더 이상 무시할 수 없었습니다.
처음 배운 것은 Vue입니다. 구성 요소와 상태, 기타 모든 것이 완전히 새로운 멘탈 모델이었기 때문에 엄청나게 힘든 시간을 보냈고 모든 것을 맞추는 데 많은 고통이 있었습니다. 하지만 결국에는 그렇게 했고, 제 등을 두드려주었습니다. 축하합니다, 친구. 나는 스스로에게 말했어요. 당신은 가파른 오르막을 올랐습니다. 이제 프레임워크의 나머지 부분을 배워야 하는 경우 매우 쉬울 것입니다.
그래서 어느 날 React를 배우기 시작했을 때 내가 얼마나 잘못했는지 깨달았습니다. Facebook은 Hooks를 삽입하고 모두에게 "이제부터 이것을 사용하십시오. 그러나 클래스를 다시 작성하지 마십시오. 수업은 괜찮습니다. 사실 그렇게 많지는 않지만 괜찮습니다. 그러나 Hooks는 전부이며 미래입니다.
알았어요? 엄청난!".
결국 나도 그 산을 넘었다. 하지만 나는 React 자체만큼 중요하고 어려운 것에 부딪쳤습니다. 바로 render 입니다.

React에서 렌더링과 그 미스터리를 접했다면 내가 무슨 말을 하는지 알 것입니다. 그리고 당신이하지 않았다면 당신을 위해 무엇을 준비하고 있는지 알 수 없습니다!
하지만 어떤 일에 시간을 낭비하기 전에 그로부터 무엇을 얻을 수 있는지 물어보는 것이 좋은 습관입니다. React 개발자로서의 삶이 이 렌더링에 대해 걱정하지 않고 잘 움직이고 있다면 왜 신경을 쓰겠습니까? 좋은 질문입니다. 먼저 이에 대한 답을 하고 렌더링이 실제로 무엇인지 살펴보겠습니다.
React에서 렌더링 동작을 이해하는 것이 왜 중요한가요?
우리는 모두 JSX라는 것을 반환하는 (요즘에는 기능적인) 구성 요소를 작성하여 React를 배우기 시작합니다. 우리는 또한 이 JSX가 페이지에 표시되는 실제 HTML DOM 요소로 어떻게든 변환된다는 것을 이해합니다. 상태 업데이트에 따라 페이지가 업데이트되고 경로가 예상대로 변경되며 모든 것이 정상입니다. 그러나 React가 어떻게 작동하는지에 대한 이러한 관점은 순진하고 많은 문제의 근원입니다.
우리는 종종 완전한 React 기반 앱을 작성하는 데 성공하지만 애플리케이션의 특정 부분(또는 전체 애플리케이션)이 현저하게 느린 것을 발견할 때가 있습니다. 그리고 최악의 부분. . . 우리는 단 한 가지 단서가 없습니다! 모든 것을 올바르게 수행했으며 오류나 경고가 표시되지 않으며 구성 요소 설계, 코딩 표준 등의 모든 모범 사례를 따랐으며 네트워크 속도 저하나 비용이 많이 드는 비즈니스 논리 계산이 뒤에서 진행되지 않습니다.
때로는 완전히 다른 문제입니다. 성능에는 아무런 문제가 없지만 앱이 이상하게 작동합니다. 예를 들어 인증 백엔드에 대해 세 번의 API 호출을 수행하지만 다른 모든 것에 대해서는 한 번만 호출합니다. 또는 일부 페이지가 두 번 다시 그려지고 동일한 페이지의 두 렌더링 사이에 가시적인 전환이 발생하여 불안정한 UX가 생성됩니다.

무엇보다 이런 경우에는 외부의 도움을 받을 수 없습니다. 좋아하는 개발자 포럼에 가서 이 질문을 하면 그들은 “앱을 보지 않고는 말할 수 없습니다. 여기에 최소한의 작업 예를 첨부할 수 있습니까?” 글쎄요, 물론 법적인 이유로 전체 앱을 첨부할 수는 없지만, 그 부분의 작은 작업 예제에는 실제 앱에서와 같이 전체 시스템과 상호 작용하지 않기 때문에 해당 문제가 포함되지 않을 수 있습니다.
나사로 죈? 예, 당신이 나에게 묻는다면.
따라서 그러한 비참한 날을 보고 싶지 않다면 이해력과 관심을 키우는 것이 좋습니다. 마지 못해 얻은 이해는 React 세계에서 멀리 가지 못할 것입니다. 저를 믿으세요. 이해하기 어렵지 않고 마스터하기가 매우 어렵지만 모든 구석구석을 알 필요 없이 아주 멀리 갈 수 있습니다.
React에서 렌더링은 무엇을 의미합니까?
친구야, 그것은 훌륭한 질문이다. 우리는 React를 배울 때 그것을 묻지 않는 경향이 있습니다. 왜냐하면 "render"라는 단어가 우리를 잘못된 친숙함으로 이끌기 때문입니다. 사전의 의미는 완전히 다르지만(이 논의에서는 중요하지 않습니다), 우리 프로그래머는 이미 그것이 무엇을 의미해야 하는지에 대한 개념을 가지고 있습니다. 화면, 3D API, 그래픽 카드 및 제품 사양 읽기 작업을 통해 "렌더링"이라는 단어를 읽을 때 "그림 그리기"에 따라 무언가를 생각하도록 마음을 훈련시킵니다. 게임 엔진 프로그래밍에는 렌더러가 있습니다. 렌더러의 유일한 임무는 씬이 넘겨준 대로 세상을 그리는 것입니다.
그래서 우리는 React가 무언가를 "렌더링"할 때 모든 구성 요소를 수집하고 웹 페이지의 DOM을 다시 칠한다고 생각합니다. 그러나 React 세계에서(그리고 공식 문서에서도 그렇습니다) 렌더링은 그렇지 않습니다. 이제 안전 벨트를 조이고 React 내부에 대해 자세히 알아보도록 하겠습니다.

React는 가상 DOM이라는 것을 유지 관리하고 주기적으로 실제 DOM과 비교하고 필요에 따라 변경 사항을 적용한다는 것을 들어보셨을 것입니다. (이것이 바로 jQuery와 React를 함께 던질 수 없는 이유입니다. DOM). 이제 이 가상 DOM은 실제 DOM처럼 HTML 요소로 구성되지 않고 React 요소로 구성됩니다. 차이점이 뭐야? 좋은 질문! 작은 React 앱을 만들고 직접 확인해 보시지 않겠습니까?
이 목적을 위해 매우 간단한 React 앱을 만들었습니다. 전체 코드는 몇 줄을 포함하는 단일 파일입니다.
import React from "react"; import "./styles.css"; export default function App() { const element = ( <div className="App"> <h1>Hello, there!</h1> <h2>Let's take a look inside React elements</h2> </div> ); console.log(element); return element; }우리가 여기서 무엇을 하고 있는지 알아차리셨나요?
예, 단순히 JSX 요소가 어떻게 생겼는지 로깅합니다. 이러한 JSX 표현식과 구성 요소는 수백 번을 작성했지만 무슨 일이 일어나고 있는지 거의 주의를 기울이지 않습니다. 브라우저의 개발 콘솔을 열고 이 앱을 실행하면 다음과 같이 확장되는 Object 가 표시됩니다.

이것은 위협적으로 보일 수 있지만 몇 가지 흥미로운 세부 사항에 유의하십시오.
- 우리가 보고 있는 것은 DOM 노드가 아닌 일반 일반 JavaScript 객체입니다.
-
props속성에는App의className(코드에 설정된 CSS 클래스)이 있고 이 요소에 두 개의 하위 요소가 있다고 표시됩니다(이 항목도 일치하며 하위 요소는<h1>및<h2>태그임). . -
_source속성은 소스 코드가 요소의 본문을 시작하는 위치를 알려줍니다. 보시다시피, 파일 이름을App.js로 지정하고 6행을 언급합니다. 코드를 다시 보면 6행이 여는 JSX 태그 바로 뒤에 있다는 것을 알 수 있습니다. 이는 의미가 있습니다. JSX 괄호 에는 React 요소가 포함됩니다. 나중에React.createElement()호출로 변환하는 역할을 하므로 일부가 아닙니다. -
__proto__속성은 이 객체가 모든 객체를 파생시킨다는 것을 알려줍니다. 루트 JavaScriptObject의 속성을 사용하여 여기에서 보고 있는 것이 일상적인 JavaScript 객체라는 생각을 다시 한 번 강조합니다.
그래서 이제 우리는 소위 가상 DOM이 실제 DOM처럼 보이지 않고 그 시점의 UI를 나타내는 React(JavaScript) 객체의 트리라는 것을 이해합니다.

탈진한?
저를 믿으세요. 저도 그렇습니다. 이 아이디어를 가능한 한 최선의 방법으로 시도하고 제시하기 위해 계속해서 이러한 아이디어를 뒤집고, 그것들을 꺼내고 재정렬할 단어를 생각하는 것은 쉬운 일이 아닙니다.
하지만 우리는 주의가 산만해지고 있습니다!
여기까지 살아남았으므로 이제 우리는 우리가 추구했던 질문에 답할 수 있는 위치에 있습니다. React에서 렌더링이란 무엇입니까?
글쎄요, 렌더링은 가상 DOM을 탐색하고 현재 상태, 소품, 구조, UI에서 원하는 변경 사항 등을 수집하는 React 엔진 프로세스입니다. 이제 React는 일부 계산을 사용하여 가상 DOM을 업데이트하고 새 결과를 실제 DOM과 비교합니다. 페이지에서. 이 계산과 비교는 React 팀이 공식적으로 "조정"이라고 부르는 것이며, 그들의 아이디어와 관련 알고리즘에 관심이 있다면 공식 문서를 확인할 수 있습니다.
헌신할 시간!
렌더링 부분이 완료되면 React는 DOM에 필요한 변경 사항을 적용하는 "커밋"이라는 단계를 시작합니다. 이러한 변경 사항은 동기식으로 적용되고(동시에 작동하는 새로운 모드가 곧 예상되지만) DOM이 업데이트됩니다. React가 이러한 변경 사항을 적용하는 정확한 시기와 방법은 우리의 관심사가 아닙니다. 이는 완전히 내부에 있으며 React 팀이 새로운 것을 시도함에 따라 계속 변경될 가능성이 있기 때문입니다.
React 앱의 렌더링 및 성능
우리는 지금까지 렌더링이 정보 수집을 의미하며 매번 시각적 DOM 변경을 초래할 필요가 없다는 것을 이해했습니다. 우리는 또한 우리가 "렌더링"이라고 생각하는 것이 렌더링과 커밋을 포함하는 2단계 프로세스라는 것을 알고 있습니다. 이제 React 앱에서 렌더링(그리고 더 중요하게는 재렌더링)이 트리거되는 방식과 세부 정보를 모르면 앱 성능이 저하될 수 있는 방법을 살펴보겠습니다.
상위 구성요소 변경으로 인한 재렌더링
React의 부모 구성 요소가 변경되면(예: state 또는 props가 변경되어) React는 이 부모 요소 아래로 전체 트리를 안내하고 모든 구성 요소를 다시 렌더링합니다. 애플리케이션에 중첩된 구성 요소가 많고 상호 작용이 많은 경우 상위 구성 요소를 변경할 때마다 자신도 모르는 사이에 성능이 크게 저하됩니다(변경하려는 상위 구성 요소일 뿐이라고 가정).
사실, 렌더링은 React가 실제 DOM을 변경하게 하지 않습니다. 조정하는 동안 이러한 구성 요소에 대해 변경된 사항이 없음을 감지하기 때문입니다. 그러나 여전히 CPU 시간과 메모리 낭비이며 얼마나 빨리 합산되는지 놀라게 될 것입니다.

컨텍스트 변경으로 인한 재렌더링
React의 Context 기능은 모두가 가장 좋아하는 상태 관리 도구인 것 같습니다(전혀 구축되지 않은 기능). 모든 것이 매우 편리합니다. 컨텍스트 제공자에서 최상위 구성 요소를 래핑하기만 하면 나머지는 간단합니다! 대부분의 React 앱은 이와 같이 빌드되지만, 지금까지 이 기사를 읽었다면 무엇이 잘못되었는지 발견했을 것입니다. 예, 컨텍스트 개체가 업데이트될 때마다 모든 트리 구성 요소의 대규모 재렌더링이 트리거됩니다.
대부분의 앱에는 성능 인식 기능이 없기 때문에 아무도 눈치채지 못하지만, 이전에 말했듯이 이러한 간과는 대용량, 상호 작용이 많은 앱에서 많은 비용이 소요될 수 있습니다.
React 렌더링 성능 향상
이 모든 것을 감안할 때 앱의 성능을 향상시키기 위해 무엇을 할 수 있습니까? 우리가 할 수 있는 일이 몇 가지 있다는 것이 밝혀졌지만, 우리는 기능적 구성요소의 맥락에서만 논의할 것이라는 점에 유의하십시오. 클래스 기반 구성 요소는 React 팀에서 매우 권장하지 않으며 곧 나올 예정입니다.
상태 관리를 위해 Redux 또는 유사한 라이브러리 사용
Context의 빠르고 더러운 세계를 좋아하는 사람들은 Redux를 싫어하는 경향이 있지만 이것은 정당한 이유 때문에 엄청나게 인기가 있습니다. 그리고 이러한 이유 중 하나는 성능입니다. Redux의 connect() 함수는 필요에 따라 해당 구성 요소만 올바르게 렌더링하므로 (거의 항상) 마법과 같습니다. 예, 표준 Redux 아키텍처를 따르기만 하면 성능이 무료로 제공됩니다. Redux 아키텍처를 채택하면 대부분의 성능(및 기타) 문제를 즉시 피할 수 있다고 해도 과언이 아닙니다.
memo() 를 사용하여 구성 요소를 "고정"합니다.
"memo"라는 이름은 캐싱을 위한 멋진 이름인 Memoization에서 따왔습니다. 그리고 캐싱을 많이 접하지 않았다면 괜찮습니다. 여기에 간략한 설명이 있습니다. 계산/연산 결과가 필요할 때마다 이전 결과를 유지하던 곳을 살펴봅니다. 당신이 그것을 찾으면, 단순히 그 결과를 반환하십시오; 그렇지 않은 경우 해당 작업/계산을 수행하십시오.
memo() 로 바로 들어가기 전에 먼저 React에서 불필요한 렌더링이 어떻게 발생하는지 봅시다. 우리는 간단한 시나리오로 시작합니다. 사용자가 서비스/제품을 좋아한 횟수를 보여주는 앱 UI의 작은 부분(사용 사례를 수락하는 데 문제가 있는 경우 Medium에서 어떻게 "박수"할 수 있는지 생각해보십시오. "를 여러 번 눌러 기사를 얼마나 지지/좋아하는지 보여줍니다).
좋아요를 1씩 늘릴 수 있는 버튼도 있습니다. 마지막으로 사용자에게 기본 계정 세부 정보를 표시하는 또 다른 구성 요소가 있습니다. 따라가기 어렵다고 느끼더라도 전혀 걱정하지 마십시오. 이제 모든 것에 대한 단계별 코드를 제공하고(많지 않음), 마지막에는 작동 중인 앱을 엉망으로 만들고 이해를 높일 수 있는 플레이그라운드에 대한 링크를 제공합니다.
먼저 고객 정보에 대한 구성 요소를 살펴보겠습니다. 다음 코드가 포함된 CustomerInfo.js 라는 파일을 생성해 보겠습니다.
import React from "react"; export const CustomerInfo = () => { console.log("CustomerInfo was rendered! :O"); return ( <React.Fragment> <p>Name: Sam Punia</p> <p>Email: [email protected]</p> <p>Preferred method: Online</p> </React.Fragment> ); };멋진 건 없겠죠?
사용자가 앱과 상호 작용할 때 변경되지 않을 것으로 예상되는 일부 정보 텍스트(props를 통해 전달되었을 수 있음)(순수주의자의 경우에는 변경될 수 있지만 요점은 나머지 항목과 비교할 때 응용 프로그램의 경우 실질적으로 정적임). 그러나 console.log() 문을 주목하십시오. 이것은 구성 요소가 렌더링되었음을 알 수 있는 단서가 됩니다.
따라서 테스트 중에 브라우저 콘솔에 이러한 메시지가 표시되지 않으면 구성 요소가 전혀 렌더링되지 않은 것입니다. 10번 나타나는 것을 보면 구성 요소가 10번 렌더링되었음을 의미합니다. 등등.
이제 주요 구성 요소가 이 고객 정보 구성 요소를 사용하는 방법을 살펴보겠습니다.
import React, { useState } from "react"; import "./styles.css"; import { CustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <CustomerInfo /> </div> </div> ); } 따라서 App 구성 요소에 useState() 후크를 통해 관리되는 내부 상태가 있음을 알 수 있습니다. 이 상태는 사용자가 서비스/사이트를 좋아한 횟수를 계속 계산하며 처음에는 0으로 설정됩니다. React 앱이 진행되는 한 도전적인 것은 없습니다. 그렇죠? UI 측면에서 보면 다음과 같습니다.

적어도 나에게는 버튼이 부숴지지 않도록 너무 유혹적으로 보입니다! 하지만 그 전에 브라우저의 개발 콘솔을 열고 지울 것입니다. 그런 다음 버튼을 몇 번 부수고 다음과 같이 표시합니다.

버튼을 19번 눌렀고 예상대로 총 좋아요 수가 19개입니다. 색 구성표로 인해 읽기가 너무 어려워 빨간색 상자를 추가하여 주요 사항인 <CustomerInfo /> 구성 요소를 강조 표시했습니다. 20번 렌더링되었습니다!
왜 20?
모든 것이 처음에 렌더링되었을 때 한 번, 그리고 버튼을 눌렀을 때 19번. 버튼은 <App /> 구성 요소 내부의 상태 부분인 totalLikes 를 변경하고 결과적으로 주 구성 요소가 다시 렌더링됩니다. 그리고 이 게시물의 이전 섹션에서 배웠듯이 그 안의 모든 구성 요소도 다시 렌더링됩니다. 이는 <CustomerInfo /> 구성 요소가 프로세스에서 변경되지 않았지만 아직 렌더링 프로세스에 기여했기 때문에 원하지 않습니다.
어떻게 예방할 수 있습니까?
이 섹션의 제목에서 알 수 있듯이 memo() 함수를 사용하여 <CustomerInfo /> 구성 요소의 "보존" 또는 캐시된 복사본을 만듭니다. 메모된 구성 요소를 사용하여 React는 props를 보고 이전 props와 비교하고 변경 사항이 없으면 React는 이 구성 요소에서 새로운 "렌더링" 출력을 추출하지 않습니다.
다음 코드 줄을 CustomerInfo.js 파일에 추가해 보겠습니다.
export const MemoizedCustomerInfo = React.memo(CustomerInfo);그래, 그게 우리가 해야 할 전부야! 이제 주요 구성 요소에서 이것을 사용하고 변경 사항이 있는지 확인할 차례입니다.
import React, { useState } from "react"; import "./styles.css"; import { MemoizedCustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <MemoizedCustomerInfo /> </div> </div> ); }예, 두 줄만 변경되었지만 어쨌든 전체 구성 요소를 보여주고 싶었습니다. UI 측면에서 변경된 사항은 없으므로 새 버전을 사용하고 좋아요 버튼을 몇 번 누르면 다음과 같이 표시됩니다.

그렇다면 얼마나 많은 콘솔 메시지가 있습니까?
하나만! 이것은 초기 렌더링을 제외하고 구성 요소가 전혀 건드리지 않았음을 의미합니다. 정말 대규모 앱의 성능 향상을 상상해보십시오! 좋아, 좋아, 내가 약속한 코드 놀이터에 대한 링크가 여기에 있습니다. 이전 예제를 복제하려면 CustomerInfo.js 에서 MemoizedCustomerInfo 대신 CustomerInfo 를 가져와 사용해야 합니다.
즉, memo() 는 어디에나 뿌리고 마법 같은 결과를 기대할 수 있는 마법의 모래가 아닙니다. memo() 를 과도하게 사용하면 앱에 까다로운 버그가 발생할 수 있으며 때로는 일부 예상 업데이트가 실패할 수도 있습니다. "조기" 최적화에 대한 일반적인 조언도 여기에 적용됩니다. 먼저 직감에 따라 앱을 빌드합니다. 그런 다음 어떤 부분이 느린지 확인하기 위해 집중적인 프로파일링을 수행하고 메모된 구성 요소가 올바른 솔루션인 경우에만 이를 도입하십시오.
"지능형" 구성 요소 설계
1) 지능은 매우 주관적이고 상황에 따라 다릅니다. 2) 지적인 행동은 종종 불쾌한 결과를 가져온다고 가정합니다. 따라서 이 섹션에 대한 제 조언은 다음과 같습니다. 자신이 하는 일에 너무 자신하지 마십시오.
이를 통해 렌더링 성능을 향상시킬 수 있는 한 가지 가능성은 구성 요소를 약간 다르게 설계하고 배치하는 것입니다. 예를 들어 자식 구성 요소를 리팩토링하고 계층 구조의 상위 어딘가로 이동하여 재렌더링을 피할 수 있습니다. "ChatPhotoView 구성 요소는 항상 Chat 구성 요소 안에 있어야 합니다"라는 규칙은 없습니다. 특별한 경우(그리고 성능이 영향을 받는다는 데이터 기반 증거가 있는 경우), 규칙을 어기거나 위반하는 것은 실제로 좋은 아이디어가 될 수 있습니다.
결론
일반적으로 React 앱을 최적화하기 위해 훨씬 더 많은 작업을 수행할 수 있지만 이 문서는 렌더링에 관한 것이므로 토론 범위를 제한했습니다. 그럼에도 불구하고, 이제 내부적으로 React에서 무슨 일이 일어나고 있는지, 렌더링이 실제로 무엇인지, 그리고 이것이 애플리케이션 성능에 어떤 영향을 미칠 수 있는지에 대해 더 나은 통찰력을 얻으셨기를 바랍니다.
다음으로 React Hooks가 무엇인지 알아볼까요?
