React의 가상 DOM, Vue의 가상 DOM과 비교

 

가상 DOM이란?

DOM (Document Object Model)

브라우저가 제공하는 문서 객체 모델

HTML 요소를 트리 구조로 표현하며, 브라우저에서 렌더링되는 실제 DOM을 말한다.

실제 DOM은 업데이트가 발생할때마다 전체 DOM 구조를 다시 계산하고 렌더링한다.

 

가상 DOM (Virtual DOM)

메모리 상에서 실제 DOM의 경량화된 사본을 만드는 개념으로 DOM 상태를 Javascript 객체로 나타내어 빠르게 연산할수 있다. 실제 DOM 조작 전에 변경 사항을 비교하고 필요한 부분만 실제 DOM에 업데이트 하는 방식으로 성능 최적화를 하기 위하여 생성된다.

리액트는 가상 DOM을 업데이트 할때 최소한의 DOM 업데이트를 보장하기 때문에 복잡한 업데이트가 빈번하게 발생해도 실제로 더 빠르게 업데이트할 수 있고, 유지보수에도 용이하다.

 

 

 

React의 가상 DOM

React의 가상 DOM과 비교 알고리즘 

가상 DOM은 메모리상에 존재하는 실제 DOM의 경량 사본으로 리액트는 컴포넌트의 상태나 Props가 변경될 때마다 새로운 가상 DOM을 생성한다.

리액트는 이전의 가상 DOM과 새로 생성된 가상 DOM을 비교(diff)하여 변경된 부분만 계산하는데, 이때 변경이 없으면 건드리지 않고 변경이 있는 부분만 찾아내어 실제 DOM에 반영한다.

<div>
<p>텍스트만 변경될 경우</p>
</div>

텍스트만 변경될 경우 <p> 태그 노드는 유지되고 노드 내부의 텍스트 노드만 변경한다.

 

재조정(Reconciliation)

상태나 Props가 변경되면, UI를 효율적으로 다시 그리는 과정을 말한다.

DOM 트리구조를 위에서 아래로 순차적으로 비교하여(전체 비교는 아니다) 변경된 노드만 업데이트한다.

배열이나 리스트일 경우 key를 사용하여 재사용 여부를 판단해 불필요한 업데이트를 방지한다. 이때 key가 같으면 노드를 재사용하며 다를 경우 새로 생성한다.

 

React의 가상 DOM 업데이트 과정

1. 상태 또는 props 업데이트를 감지하게되면 해당 컴포넌트의 렌더링 함수가 호출된다. (리렌더링)

2. 변경된 상태를 기반으로 새로운 가상 DOM 생성한다.

3. 이전 가상 DOM과 2번에서 만들어진 가상 DOM을 비교하게되는데, 이때 재귀적 비교를 통해 변경된 노드를 찾는다. 이때 전체 가상 DOM을 비교하는게 아니라 최적화 방식으로 특정된 영역의 가상 DOM만을 비교한다.

    ㄴ 동일한 레벨에서 노드 유형이 동일한 경우에만 비교하며, 노드 유형이 다를 경우는 해당 노드를 완전히 교체한다.

    ㄴ key를 활용하여 고유성을 식별하며, key가 없거나 중복되면 리스트를 다시 비교하여 교체한다. (비효율적임)

    ㄴ 동일한 노드와 동일한 하위트리에서 변경사항이 없다고 판단되면, 해당 하위 트리를 비교하지 않는다.

 4. 실제 DOM에 필요한 변경사항을 최소화하여 패치한다.

 

리액트는 컴포넌트를 다시 렌더링 한 뒤 가상 DOM 비교를 통해 효율성을 유지한다.

불필요한 리렌더링을 방지하려면 개발자가 추가적인 최적화 작업(메모이제이션)을 할수있다.

 

 

 

Vue의 가상DOM 업데이트와 비교해볼까?

vue는 종속성 추적과 반응형 데이터 시스템을 활용해 React와는 다른 방식으로 실제 DOM 업데이트를 최적화한다.

ㄴ 종속성 추적 ? 데이터와 이를 사용하는 템플릿 또는 컴포넌트간의 종속성 관계를 추적한다. 이로인해 변경된 데이터와 그에 의존하는 부분만 업데이트가 가능하다. 

ㄴ 반응형 데이터 모델 ? reactive 또는 ref 같은 API로 반응형 데이터를 관리한다. 이 데이터는 기본적으로 proxy를 활요하여 데이터의 변화가 생기면 이를 즉시 감지하고 필요한 업데이트를 수행한다.

vue는 데이터 중심, react는 컴포넌트 중심으로 가상 DOM을 다룬다고 이해하면된다.

 

vue의 가상 DOM 업데이트 과정

1. reactiva, ref등을 통해 데이터 변경을 감지한다. 어떤 데이터가 어떤 DOM 노드와 연결되어있는지 추적한다. (종속성 추적)

2. 종속된 데이터가 변경된 경우에만 가상 DOM을 생성한다

3. 이전 가상 DOM과 2번에서 생성한 가상 DOM을 비교한다. (종속된 데이터가 변경된 부분만 비교)

4. 실제 DOM에 필요한 변경사항을 최소화하여 패치한다.

 

vue의 가상 DOM이 효율적인 경우?

부분 업데이트가 많은 경우에 종속성 추적으로 필요한 부분만 업데이트가 가능하고,

반응형 데이터 시스템을 활용하면 (reactive. ref) 객체 내부 속성 변경도 쉽게 감지하기에 같은 데이터 구조를 사용할 경우  vue가 더 효율적이라고 할 수 있다.

 

 

 

정말 가상 DOM이 빠를까?

가상 DOM을 사용하는 프레임워크가 성능상의 한계를 보일수 있다는 점에서 '가상 DOM이 무겁다'라는 주제가 제기되었다고한다. 이로인해 가상 DOM을 사용하지 않거나 더 가벼운 방식을 채택한 프레임워크, 라이브러리들이 등장했다. (ex: svelte, solid.js...)

가상 DOM은 실제 DOM 조작 전 변경 사항을 계산하는 단계가 존재하고, 메모리를 사용하여 저장하고 관리해야한다.

 

항상 가상 DOM이 좋은 것은 아니다.

프로젝트 규모나, 업데이트되는 빈도 혹은 구조에 따라 실제 성능이 다를 수 있다.

예를 들어 DOM의 변경사항이 거의 없거나 간단할때에 실제 DOM이 더 빠를 수 있다.

소규모 애플리케이션에서 DOM의 변화가 거의 없을 경우, 가상 DOM의 비교(diff) 연산이나 메모리 사용등이 불필요할 수 있다. 따라서 가상 DOM이 유리한 경우는 복잡하거나 빈도 높은 업데이트가 발생할때 유리할 것이다. (대규모 어플리케이션 일수록 유리)

 

 

 

타이머, 숫자 가변폭 폰트를 고정폭 폰트로 

font-variant-numeric CSS

 

타이머 UI에서 숫자 값을 감싼 wrapper이 움직이는 문제가 있었다.

이유는 날짜와 시간이 동적으로 들어가기때문에 고정폭을 적용하지 않은 UI였고, 이 때문에 발생했다.

동영상을 잘보면 미세하게 끝이 움직인다.

 

적용 전

 

왜 이런 이슈가 생기는 걸까?

고정폭 폰트, 가변폭 폰트가 각각 존재하기 때문이다.

말 그대로 고정폭 폰트는 각 글자가 동일한 폭을 차지하며 가변폭은 글자의 폭이 동일하지 않는 폰트를 말한다.

 

이때는 어떻게 고정폭으로 적용할까?

아래 CSS만 적용하면 바로 해결된다. *물론 사용하는 폰트가 OpenType 포맷이어야함

font-variant-numeric: tabular-nums;

 

적용하고나니 숫자모두 동일한 고정폭을 가지게 되었기 때문에 시간초가 줄어도 끝이 움직이지 않는다.

 

적용 후 

 

숫자의 폭이 동일해졌으므로, 숫자 정렬이 깔끔하다.

 

font-variant-numeric의 속성에는

proportional-nums(가변폭),

tabular-nums(고정폭) 외에도

lining-nums, oldstyle-nums,, diagonal-fractions, stacked-fractions 같은 속성들이 존재한다.

 

아 자고로, 해당 속성은 OpenType 기능을 지원하는 폰트만 사용 가능하다는데....

OpenType은 폰트 포맷의 하나로 Microsoft와 Adobe가 공동으로 개발한 디지털 서체 기술이라고 한다.

단일 파일로 여러 언어와 스크립트를 지원하도록 설계되었고, 글꼴의 고급 렌더링을 지원한다. (=> font-variant-numeric도 때문에 사용 가능.)

 

css도 끝없이 공부해야 몸이 고생안하는듯 하다!

 

 

원링크 (One Link), 딥 링크(Deep Link), 디퍼드 딥 링크 (Deferred Deep Link) - 웹뷰와의 통신

초반에 잘못 이해해서 꽤나 개념을 이해하는데 쉽지 않았다.

회사, 사람마다 같은 개념을 다른형식으로 말하기도 해서 헷갈리기도 했고, 마케팅하시는 분들의 블로그에서 잘못된 정보들이 있어 이해하는데 시간이 걸렸다.

다시 정리해보자.

 

원링크 (One Link)

하나의 링크를 통해 여러 플랫폼 또는 목적지로 사용자들을 유도하는 기술이나 개념.

단일 URL로 여러 목적지를 처리하는 마케팅 기술 플랫폼.

(AppsFlyer의 통합 딥링킹 플랫폼으로 하나의 단일 링크만으로도 딥링킹 제공)

즉, 사용자가 원링크를 클릭하면, 링크는 사용자의 디바이스나 운영체제(OS)등의 정보를 분석하여 가장 적합한 목적지로 라우팅한다.

웹은 웹 사이트, iOS는 앱스토어, Android는 구글 스토어..

https://{고유한값}.onelink.me와 같은 형태의 링크다. (Branded Domains 기능이 있어서 appLink.{고유한값}.com 형태로 변경해서 사용가능하다고 하다.)

 

 

딥 링크(Deep Link)

딥링크의 주요 3가지 방식은 

  • URI 스킴 방식 (URI Scheme) 
  • 앱 링크 (App Link)
  • 유니버셜 링크 (Universal Link)

 

URI 스킴 방식

여기서 초기에 딥링크는 URI 스킴 방식만 존재했는데, URI 스킴방식은 아래와 같이 사용한다.

{scheme}://{path}?{queryString=123}

myapp://product?productId=123

이 방식은 한계를 가지고 있었는데, 앱이 설치되어 있어야만 실행가능하며, scheme은 유일하지않은 값이기때문에 중복될 수 있었다.

따라서 myapp 스킴을 여러 앱에서 사용할 수 있고 이렇게 되었을때는 충돌나며, 악의적으로 이용될수도 있다고 한다. (아무 연관이 없는 회사에서 kakao:// 이렇게 만들어버린다던가..)

 

그래서 이 URI 스킴 방식의 한계를 극복하고자, 등장하게된게 앱링크(App Link)와 유니버셜링크 (Universal Link)라고 한다 (두개는 같은 개념으로 해석하면 되고, 제공하는 곳이 다르고 앱링크는 Android, 유니버셜링크는 iOS)

-> 이 부분이 내가 헷갈린 부분 중 하나이기도 하다, 회사에서 주로 유니버셜링크로 퉁 쳐서 말하는 경우들이 발생하는 것 같았고, 앱링크 !== 유니버셜링크 로 아예 다른 개념인것으로 이해해버렸다. 

앱링크 = 유니버셜링크 => 요즘 추세로는 그냥 딥링크로 통용되는듯 하다.

 

 

앱링크 (App Link) 와 유니버셜 링크 (Universal Link)

App Link는 Android

Universal Link는 iOS 

웹 사이트의 URL 형태로 만들어진다. (https://www.{고유한주소}.com/product)

고유한 웹 사이트 주소로 만들어지기때문에 항상 유일하게 되어 URI 스킴방식의 한계였던 어뷰징이 불가능하다.

둘다 도메인이 소유자라는 것을 인증해주는 절차가 필요하다고 하는데, 웹 사이트의 특정 파일에 등록해서 인증처리하면 되는 듯 하다. 

앱이 설치되어있는 경우 해당 딥링크에 맞는 화면이 실행되고, 앱이 없으면 웹 URL경로이기때문에 웹에서 처리해주면된다 (앱마켓으로 이동, 혹은 웹에서 대응)

 

문제점은 주소 입력창에 직접 복사/붙여넣기시 동작하지 않는것과 크롬, 사파리 등..을 제외하고 각 플랫폼에서 본인들이 개발한 브라우저로 할 경우 동작하지 않는 경우들이 있다고 한다. (동작이 다른 경우도 있다고 함)

 

 

디퍼드 딥링크 (Deferred Deep Link)

앱이 설치되지 않은 상태에서도 링크 데이터를 유지하여 앱 설치 후에 데이터를 복원해 사용자를 특정 화면으로 안내하는 기능 제공

지연된 딥링크로, 딥링크를 눌렀을때 앱이 설치되어있으면 앱이 열리면서 해당 컨텐츠 실행, 앱 미설치시 앱마켓 이동 후 설치 이후 해당 컨텐츠 실행

 

 

정리

원링크 플랫폼을 통해 원링크를 생성하고, 사용자가 원링크를 클릭하는 경우에 원링크 플랫폼이 사용자 디바이스의 플랫폼을 판단하고, 앱이 있는 경우 딥링크(앱링크=유니버셜링크)를 통해 앱의 특정 화면으로 이동시킨다.

이때 앱이 없는 경우 앱마켓으로 이동시키고, 이때 디퍼드 딥링크 설정이 되어있다면 디퍼드 딥링크 기능을 통해 앱 설치 후 원하는 화면으로 이동시킨다.

-> 이 경우는 원링크 제공 업체의 기술로써 여러 정보를 토대로 유저를 식별하여, 원링크 클릭한 사용자가 디퍼드 딥링크를 사용할 경우 이 사용자의 디퍼드 딥링크 정보를 DB에 저장하게 되고, 앱 설치 이후에 정보를 제공받아 해당하는 화면으로 이동시킨다.

 

나는 하나하나 다 다른 개념이고 같은 단계라고 생각했는데, 조합이라는 의미가 이런 의미라는 것을 알게되었다. 

유익한 시간이었다.

 

Bfcache (Back-Forward Cache)

브라우저 페이지 이동 시 페이지의 상태를 완전히 캐싱해 사용자가 <뒤로가기(Back) | 앞으로가기(Forward)> 할때, 빠르게 로드할 수 있도록 하는 매커니즘

Bfcache는 페이지를 새로 로드하지 않고 캐시에서 복원하기 때문에 더 빠른 페이지를 탐색하게 하기 때문에 사용자 경험에 중요하다.

 

주요 특징

1. 전체 페이지 상태 저장

  • DOM 상태, Javascript 메모리(힙 메모리 영역까지 포함), 스크롤 위치 등을 완전한 상태로 저장 (페이지를 메모리에 보관, = 스냅샷 보관)
  • 새로 데이터를 요청하거나 렌더링하지 않고 (Javascript 재실행하지 않음) 이전 페이지의 상태를 그대로 복원
    • 네트워크 요청이 감소되어 서버 부담이 줄어듬
    • Task Queue에서 Timer Promise도 일시 정지되었다가, 복원시 이어서 실행 (=> 즉, 작업들이 모두 보존되었다가 다시 처리)
  • 뒤로 앞으로 이동 시 복원 (폼 입력이나 스크롤 위치 등도 복원됨)

2. 브라우저 수준 캐싱

  • HTTP 캐싱(브라우저 캐시)와 다르게 네트워크 요청 없이 메모리에서 페이지를 즉시 복원
  • 스크롤 위치와 필터 상태가 그대로 유지

 

활성화 확인

1. 브라우저 개발자 도구에서 Network > Disable cache 체크 해제 확인

 

2. Performance 탭 / Network 탭(-> 네트워크 요청 발생 x)

뒤로가기 > 앞으로 가기 시에 캐시 테스트 데모사이트 

페이지 전환 스냅샷을 보고 Event log에서 아래와 같이 확인 가능하다.

 

Javascript 로 확인

  • event.persisted === true이면 페이지가 Bfcache에서 복원된 것.
  • pageshow, pagehide 이벤트를 통해 Bfcache를 관찰할 수 있지만, 페이지가 Bfcache에 의해 복원되었는지 여부는 정확히 알수있지만, 페이지 이동시에는 현재 페이지를 캐시 하려고 했다는 사실만 알 수 있다고 한다.
window.addEventListener("pageshow", (event) => {
  if (event.persisted) {
    console.log("Page was loaded from Bfcache");
  } else {
    console.log("Page was loaded normally");
  }
});

 

 

Bfcache와 일반 브라우저 캐시, 같은거 아냐 ??

  • 브라우저캐시는 정적리소스(HTML, CSS, JS)를 캐싱하는데, Bfcache는 DOM 상태나 Javascript 메모리, 스크롤 위치에 대해 캐싱한다. 
  • 또한 Bfcache는 즉시 로드이므로 지연을 느끼기 어려운데 반해, 브라우저캐시는 네트워크 요청이 필요한 경우에 따라 지연이 발생할 수 있다.
    • 브라우저 캐시는 HTTP 캐싱 규칙에 따라 작동한다.
    • Bfcache는 특정 조건을 충족해야 활성화된다..

 

 

Bfcache... 제약 조건이 있다.

페이지가 완전히 캐싱될 수 있는 경우만 동작하고 이 조건에 해당하지 않으면 데이터 요청&렌더링은 당연히 발생한다.

  1. 페이지에서 명시적으로 캐싱 비활성화
    • 응답 헤더에 Cache-Control: no-store 또는 no-cache 포함할 경우
  2. 자바스크립트 코드에 unload, beforeunload 이벤트 리스너 등록한 경우
    • 실제 페이지가 새로 로드되는 것이 아니므로 load 이벤트가 발생되지 않음
    • PV 수집하는 경우 정확한 수집이 어렵다
    • unload -> pagehide 사용
  3. 오픈된 네트워크 연결 정리 (WebSocket 또는 특정 네트워크 연결이 열려있을 경우)
    • 페이지 이동 시 WebSocket이나 EventSource 연결을 닫아야 동작
  4. 일부 브라우저나 버전에서 Bfcache를 제한적으로 지원할 경우 (can i use에서 확인하기)
    • 브라우저마다 캐시가 저장되는 시간도 다르다고 하는데, 크롬은 3분이지만 상황에 따라 유지되지 않을수도 있다고 한다.
    • same-site 여부, 데스크톱/모바일 여부, https 적용 여부등에 따라서도 다른 동작할 수 있다고한다. (구체적인 조건/상황이 다름을 인지하기)
  5. 메모리가 부족한 경우

 

자 여기까지 개념을 익혔고,

사실 실무에서 고민하게 되었던 부분이 무한 스크롤링 패치에서 였다.

클라이언트 상태와 스크롤 위치가 초기화 되었는데, (useEffect로 데이터 초기화), 옵저버로 스크롤 이벤트 마다 데이터를 동적으로 가져와서 넣었기 때문에 데이터가 비어있는 상태에서 스크롤 위치가 복구되지 않는 부분이 문제였다.

초반의 서버사이드 패치로 데이터를 넣은 부분은 뒤로가기 시 다시 요청하지 않으므로, 딱 거기까지 스크롤이 복구되었다.

(스크롤 위치와 데이터 간 불일치)

 

그래서 데이터를 세션스토리지에 담고, 접근 시 복원하는 작업을 진행했다.

(이때 timestamp를 넣어줘서, 세션이어도 만료시간을 지정해주고 특정 시간이 지나면 최신화를 위해 리프레시가 발생하도록 유도했다)

-> 이렇게 하니 스크롤 위치가 복원되어 따로 스크롤 복원을 진행하지 않았다.

 

리스트를 그리고 뒤로가기/ 앞으로가기를 하면서 모든 페이지에서 이러한 방법을 채택한 것은 아니었지만(리소스에 따른 우선순위) 사용자 경험은 역시 재미있는 작업인 것 같다.

 

 

 

자바스크립트 인터프리터가 함수의 선언, 할당, 실행을 나눠해서 모든 선언이 코드의 선두로 끌어올려진 것처럼 동작하는 현상의 개념

즉, 자바스크립트 엔진이 실행컨텍스트가 활성화 될 때 변수정보를 수집하는데 

이때 실제 끌어올려지지는 않았지만 수집과정에서 엔진이 수집된 정보를 토대로 변수들을 알고 있기 때문에 끌어올려진 것으로 간주하는 개념 

 

설명보기

Q. var, let 둘 다 호이스팅이 된다? 

- 정답은 둘 다 호이스팅되나 동작때문에 var만 실행 시 에러가 나지 않는다. 이때문에 var만 호이스팅된다고 착각할 수 있다.

 

설명

var는 선언과 할당을 동시에 실행하므로 호이스팅되었을때, undefined가 메모리에 저장된다.

let은 선언과 할당을 별도로 실행한다.

따라서 호이스팅되었을때, 선언은 되었지만 할당은 되지 않아서 메모리에 저장되지 않으며 TDZ(Temporary Dead Zome)에 들어간다. 따라서 호출 시 초기화 전에는 엑세스할 수 없다는 에러가 뜨는데, 이것때문에 호이스팅 되지 않았다고 착각할 수 있다.

 

 

 

 

 

실행 컨텍스트는 실행한 코드에 제공 할 환경 정보들을 모아놓는 객체라고 생각하자.

콜 스택에 쌓아서 전체 코드를 관리하고 있어서 환경과 순서를 보장한다.

동작

실행 컨텍스트는 크게 3가지 동작을 한다.

  1. 내부 환경 정보 기록: 실행컨텍스트가 활성화 되는 시점(실행)에 선언된 변수를 수집한다. 
  2. 외부 환경 정보를 구성한다.  
  3. this 값을 설정하는 동작 구성

 

수집 정보

- 자바스크립트 엔진이 활용할 목적으로 생성할 뿐, 개발자가 코드에 접근 할 수 없다.

Variable Environment

  • 선언 시점의 Lexical Environment의 스냅샷이므로 변경사항이 반영되지 않는다.
  • Variable Environment를 생성하여 정보를 담고 복사하여 Lexical Environment를 만든다.

Lexical Environment

처음엔 Variable Environment와 같으나 변경사항이 실시간으로 반영된다. (이후에는 Lexical을 사용하는 이유)

  1. Environment Record (환경 기록)
    • 현재 컨텍스트내의 식별자(변수명, 함수명, 매개변수명....) 들에 대한 정보를 순서대로 저장한다.
    • 이때 정보의 수집을 마치게되면 실행컨텍스트가 관여할 코드들은 실행전이지만, 자바스크립트 엔진은 이미 해당 환경에 속해 있는 정보(변수명..)들을 알고 있기 때문에 '호이스팅'이 발생 할 수 있다.
  2. Outer-Environment Reference (외부 환경 참조)
    • 현재 호출된 함수가 선언된 당시 상위의 Lexical Environment를 참조한다.
    • 스코프 체인이 가능하게 하는 수집자료 (중첩된 자바스크립트 코드에서 스코프 탐색을 하기위해 사용)

This Binding

  • this 식별자가 바라봐야 할 대상 객체
  • 실행 컨텍스트 활성화 당시에 this로 지정된 객체가 저장된다. (this는 함수호출 방식 및 전역 여부에 따라 결정)



 

+ Recent posts