[번역] 리액트에 대해서 그 누구도 제대로 설명하기 어려운 것 – 왜 Virtual DOM 인가?


리액트를 지난 2년간 사용하면서도 막상 말끔하게 설명하라고 하면 어려웠던 주제, 원래 번역글은 잘 안쓰지만 글 자체가 구성이 잘 되어있어서 글을 번역해보았습니다.

원본: https://hashnode.com/post/the-one-thing-that-no-one-properly-explains-about-react-why-virtual-dom-cisczhfj41bmssp53mvfwmgrq

글의 자연성 및 더 높은 이해도 및 몰입도를 위해 의역이 많이 포함되어 있습니다 🙂

 

어느날 내 친구가 나한테 질문을 했어요: “컴포넌트를 통한 프로젝트 구성, 단방향 데이터 바인딩,  대충 알겠는데.. 왜 Virutal DOM 을 쓰는거야?”

그래서 나는 익숙한 답변을 해줬죠. “음.. 그건 DOM 조작이 비효율적이기 떄문이야. 그리고 또 느리고”

친구가 다시 되묻길, “자바스크립트 엔진은 계속해서 성능이 좋아지고 있는데, 정확히 어떤 부분 때문에 DOM 이 느려지는거야?”

 

… 쩝.

 

좋은 질문이군요! 제가 검색을 좀 해봤는데, 놀랍게도 Virtual DOM 가 왜 정말 필요한지에 대해서 명료하고 이해하기 쉽게 다루는 글은 찾을수가 없었어요.

 

정확히는, DOM 조작이 전체 동작을 비효율적으로 만드는게 아니라, 그 이후에 일어나는 일 때문에, 작업이 더뎌지는거에요.

자, Virtual DOM 을 이해하기 위해서, 잠깐 다른 개념을 좀 다뤄볼게요. 한번 브라우저의 워크플로우가 어떻게 이뤄지는지 살펴보고 DOM 조작 후 어떤일이 일어나는지 대해서 알아볼게요.

 

브라우저의 Workflow

이해를 잘 하기 위해선 브라워저가 어떻게 작동하는지 알아볼 필요가 있어요.

NOTE: 다음 다이아그램과 앞으로 나올 설명은 Webkit 엔진에서 사용하는 용어들을 사용했어요. 작동방식은 대부분의 브라우저에서 비슷합니다. 미묘한 차이가 있을 뿐이에요.

 

 

DOM Tree 생성

브라우저가 HTML 을 전달받으면, 브라우저의 렌더 엔진이 이를 파싱하고 DOM 노드(Node) 로 이뤄진 트리를 만들어요. 각 노드는 각 HTML 엘리먼트들과 연관되어있죠.

 

Render Tree 생성

그리고, 외부 CSS 파일과 각 엘리먼트의 inline 스타일을 파싱해요. 스타일 정보를 사용하여 DOM 트리에 따라 새로운 트리, 렌더트리를 만들어요.

 

Render Tree 생성 – 그 뒤에선 무슨일이 일어나고 있는가..?

Webkit 에서는 노드의 스타일을 처리하는 과정을 ‘attachment’ 라고 불러요. DOM 트리의 모든 노드들은 ‘attach’ 라는 메소드가 있어요. 이 메소드는 스타일 정보를 계산해서 객체형태로 반환합니다.

이 과정은 동기적(synchronous) 작업이구요, DOM 트리에 새로운 노드가 추가되면 그 노드의 attach 메소드가 실행됩니다.

렌더 트리를 만드는 과정에선, 각 요소들의 스타일이 계산되구요, 또 이 계산되는 과정에서 다른 요소들의 스타일 속성들을 참조합니다.

 

Layout (reflow 라고도 불립니다)

렌더 트리가 다 만들어지고 나면, 레이아웃 과정을 거쳐요. 각 노드들은 스크린의 좌표가 주어지고, 정확히 어디에 나타나야 할 지 위치가 주어집니다.

 

Painting

그 다음 작업은 렌더링 된 요소들에 색을 입히는 과정입니다. 트리의 각 노드들을 거쳐가면서 paint() 메소드를 호출해요. 그러고나면, 스크린에 원하는 정보가 나타나는거죠.

 

Virtual DOM 을 만나보세요

자 이제 DOM 을 조작했을 때 어떤 작업이 이뤄지는지 알겠죠? DOM에 변화생기면, 렌더트리를 재생성하고 (그러면 모든 요소들의 스타일이 다시 계산됩니다) 레이아웃을 만들고 페인팅을 하는 과정이 다시 반복되는거죠.

복잡한 SPA(싱글 페이지 어플리케이션) 에서는 DOM 조작이 많이 발생해요. 그 뜻은 그 변화를 적용하기 위해 브라우저가 많이 연산을 해야한단 소리고, 전체적인 프로세스를 비효율적으로 만듭니다.

자, 이 이부분에서 Virtual DOM 이 빛을 발합니다! 만약에 뷰에 변화가 있다면, 그 변화는 실제 DOM 에 적용되기전에 가상의 DOM 에 먼저 적용시키고 그 최종적인 결과를 실제 DOM 으로 전달해줍니다. 이로써, 브라우저 내에서 발생하는 연산의 양을 줄이면서 성능이 개선되는 것 이지요.

 

업데이트: ugwe43to874nf4 라는 레딧유저님이 Virtual DOM 의 중요성을 더 알려주었습니다.

DOM 조작의 실제 문제는 각 조작이 레이아웃 변화, 트리 변화와 렌더링을 일으킨다는겁니다. 그래서, 예를 들어 여러분이 30개의 노드를 하나 하나 수정하면, 그 뜻은 30번의 (잠재적인) 레이아웃 재계산과 30번의 (잠재적인) 리렌더링을 초래한다는 것이죠.

Virtual DOM 은 그냥 뭐 엄청 새로운것도 아니고, 그냥 DOM 차원에서의 더블 버퍼링이랑 다름이 없는거에요. 변화가 일어나면 그걸 오프라인 DOM 트리에 적용시키죠. 이 DOM 트리는 렌더링도 되지 않기때문에 연산 비용이 적어요. 연산이 끝나고나면 그 최종적인 변화를 실제 DOM 에 던져주는거에요. 딱 한번만 한는거에요. 모든 변화를 하나로 묶어서. 그러면, 레이아웃 계산과 리렌더링의 규모는 커지겠지만, 다시 한번 강조하자면 딱 한번만 하는거에요. 바로 이렇게, 하나로 묶어서 적용시키는것이, 연산의 횟수를 줄이는거구요.

사실, 이 과정은 Virtual DOM 이 없이도 이뤄질수 있어요. 그냥, 변화가 있을 때, 그 변화를 묶어서 DOM fragment 에 적용한 다음에 기존 DOM 에 던져주면 돼요.

그러면, Virtual DOM 이 해결 하려고 하는건 무엇이냐? 그 DOM fragment를 관리하는 과정을 수동으로 하나하나 작업 할 필요 없이, 자동화하고 추상화하는거에요. 그 뿐만 아니라, 만약에 이 작업을 여러분들이 직접 한다면, 기존 값 중 어떤게 바뀌었고 어떤게 바뀌지 않았는지 계속 파악하고 있어야하는데 (그렇지 않으면 수정 할 필요가 없는 DOM 트리도 업데이트를 하게 될 수도 있으니까요), 이것도 Virtual DOM 이 이걸 자동으로 해주는거에요. 어떤게 바뀌었는지 , 어떤게 바뀌지 않았는지 알아내주죠.

마지막으로, DOM 관리를 Virtual DOM 이 하도록 함으로써, 컴포넌트가 DOM 조작 요청을 할 때 다른 컴포넌트들과 상호작용을 하지 않아도 되고, 특정 DOM 을 조작할 것 이라던지, 이미 조작했다던지에 대한 정보를 공유 할 필요가 없습니다. 즉, 각 변화들의 동기화 작업을 거치지 않으면서도 모든 작업을 하나로 묶어줄 수 있다는거죠.

 

더 읽어보기

여기서 서술한 브라우저 작동방식에 대한 정보는 여기서 발췌했습니다. 브라우저의 작동방식에 대해서 더 깊게 다루는 포스트에요. 읽어볼만한 가치가 있습니다. 제가 Virtual DOM 의 필요성을 이해하게 해주는데 큰 도움이 되었어요.

– 포스트 끝 –


추가 내용

리액트 입문자들 중에서 일부 사람들이 오해하고 있는것이, 리액트 = 빠르다! 인데, 이는 잘못된 인식입니다.

한번 Redux 창시자이자 React 개발팀원인 Dan Abramov 의 트윗을 한번 읽어볼까요?

정말 명료한 말입니다.

번역: React가 DOM 보다 빠르다는건 잘못된 사실이에요. 사실은: 유지보수 가능한 어플리케이션을 만드는것을 도와주고 그리고 대부분의 경우에 ‘충분히 빠르다’

 

충분히 빠르기에 또 페이스북에서 쓰는거구요.

 

최적화 작업을 (제대로) 손수했을때와 리액트를 사용 했을때를 비교한다면 대부분의 경우 전자가 더 빠릅니다.

하지만 이를 자동화해주는 리액트를 사용 했을 땐 생산성이 배가되겠죠?

 

추가적으로, 리액트를 사용한다고해도 최적화작업이 제대로 이뤄지지 않으면 오히려 엄청~~나게 구린 퍼포먼스를 나타낼수도 있다는 것, 꼭 알아두세요~

  • jason0853

    리액트에 대해서 잘못 알고 있었네요.ㅠㅠ 포스팅 감사합니다.

  • 좋아요👍

  • Kitchu Lee

    좋은 글 감사합니다. : )

  • Min-Gyu Choi

    최적화 안하면 퍼포먼스 차이가 하늘과 땅 차이죠 ㅋㅋㅋ

  • Dongmin Jang

    맨마지막에 언급 된 최적화 작업에 대해서 예시를 들어 설명해 주실 분 있을까요?

    • 예를들어 무한스크롤링 pagination을 할 경우에, shouldComponentUpdate 를 통한 최적화작업을 하지 않으면
      새 데이터가 로딩 될 때 기존에 이미 렌더링된 애들도 새로 렌더링되게 됩니다. 이걸 꼭 막아주셔야해요

      추가적으로 key설정을 해서, 만약에 중간에 데이터를 삭제해도 기존에 렌더링한건 그대로 유지하도록 해야하구요.

  • Kyeongmo Noh

    민준님에게 거의 수강료를 지급해야할 것 같은 느낌이 들 정도로 블로그 보며 많이 배우고 있습니다..ㅎㅎ(아남타워에서 열린 Javascript 컨퍼런스에서도 설명 들은 것 같은데..!)
    이번에도 마지막 Dan의 트윗을 보고 한대 얻어맞은 것 같네요. React의 핵심은 maintainable이었군요.
    읽으면서 두가지 궁금증이 생겼는데, Virtual DOM을 사용함으로써 변경이 생겼을때 변경사항을 한데 묶어 단 한번의 레이아웃과 리렌더링을 해 DOM 조작의 효율성을 높인 것으로 이해했습니다. 이 설명만 놓고 보면 마치 Database의 transaction 개념을 보는듯 하네요. 물론 많이 다른 개념이지만 변경 행위들을 하나로 묶어 원자성을 띄기 해 한번의 처리를 거치게 한다는 점에서 비슷한 것 같은데 어떻게 생각하시나요 ㅎㅎ
    그리고 여러 변경을 한데 묶어 한번의 렌더링만 되는 걸로 이해했는데 궁금한 점이, 그 여러 변경이 꼭 동시에 일어난 것인가요? 아니면 시간에 따라 예를 들어 0.2초 내에 다섯개의 변경이 있다면 그것도 한번의 묶음으로 처리될 지가 궁금하네요. 그 한데 묶음의 시간적 기준이 있는지 궁금해졌는데 민준님은 어떻게 생각하시는지 궁금합니다.

    • 리액트를 사용 할 때에, 0.2초내에 다섯개 변경이 있다면, (만약에 setState를 한다거나 redux로 action 을 dispatch 하는것을 여러번 통해서 변경을 한다면, 다섯번 렌더링합니다.

      중요한부분은, 한번 렌더링 되는 과정에서 여러곳이 바뀌는거라면 한번에 업데이트를 하는거구요.

      프로젝트 개발을 하다보면 0.2초에 한번 업데이트 한다고 하며는.. 사실 엄청난 버벅임이 느껴진답니다

      • Kyeongmo Noh

        아 동시에 일어난 변경들을 한번에 처리하는 것이군요. 답변 감사합니다!