You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
브라우저에 로드된 HTML의 태그를 자바스크립트는 객체로 다룰 수 있다. 일반적으로 document.querySelector('선택할 태그의 CSS 셀렉터') 또는 document.getElementByID('선택할 태그의 아이디') 또는 document.getElementsByClassName('선택할 태그의 클래스 속성 이름') 등의 브라우저에서 제공하는 문서 객체 모델(HTML 문서의 태그들을 자바스크립트의 객체로 불러서 컨트롤 한다는 의미)를 통해서 태그를 자바스크립트로 다룰 수 있는 기능을 제공한다.
브라우저에 로드된 태그는 자바스크립트의 문서 객체 모델을 통해서 객체로 반환되는데 Node라는 타입의 객체가 된다. 선택된 태그는 Node라는 객체에 들어 있는 속성을 모두 사용할 수 있다. 태그를 자바스크립트로 컨트롤 할 수 있는 객체를 문서 객체라고 하자. 그러면 문서_객체.parentNode, 문서_객체.childNodes, 문서_객체.firstChild, 문서_객체.lastChild등의 방식으로 Node 타입의 객체의 속성을 사용할 수 있다.
브라우저에 로드된 태그의 종류에 따라서 Node 타입의 객체의 속성만 사용할 수 있는 것이 아니라, Element 타입의 객체의 속성도 사용할 수 있다. 문서_객체.innerHTML, 문서_객체.getAttribute(), 문서_객체.querySelector(), 문서_객체.className 등의 방식으로 Element 타입 객체의 속성을 사용할 수 있다.
Node 타입의 객체가 각 태그 간의 위치 관계를 파악할 수 있는 기능을 제공하는 것과 달리 Element 타입의 객체는 우리가 일반적으로 자바스크립트 태그의 어떤 속성 값을 가져오거나 속성과 속성에 값을 부여할 수 있는 기능을 제공한다.
문서 객체 모델은 브라우저에 로드된 태그를 자바스크립트에서 컨트롤 하기 위해서 사용하는 브라우저에서 제공하는 기능이다.
리액트는 상태변경을 통해서 컴포넌트 함수를 다시 실행하고, 컴포넌트 함수가 다시 실행되면서 JSX 태그를 변화시키고, 변화된 JSX 태그가 이전에 랜더링된 태그 구조와 비교했을 때 변경 사항이 있으면 해당 부분의 태그 또는 태그의 속성을 교체하는 방식으로 동작한다.
리액트에서 태그를 변경하는 방식은 기본적으로 상태 변경을 기준으로 이뤄지며, 문서 객체 모델을 직접 사용하여 태그의 값을 변경하는 방식을 사용하지 않는다.
리액트는 이전에 저장해 둔 랜더링된 태그 구조와 컴포넌트 함수의 재실행으로 인해 랜더링 될 태그 구조의 차이를 비교하여 변경 사항이 있으면 변경하게 된다. 하지만, 리액트의 컴포넌트 함수에 의해 랜더링 되는 값에 의한 변경이 아니라 다른 요인에 의해 태그의 값을 변경하는 경우에는 이전 랜더링 때의 태그 구조와 다음 랜더링 할 태그 구조를 비교하므로 외부 요인에 의해 변경된 태그는 이전 랜더링 된 태그의 구조에 속하지 않으므로 비교에서 누락된다.
예를 들어 어떤 리액트의 상태 변화에 따라서 변화 값을 표시하는 태그가 있다고 하자. 이 태그를 브라우저의 콘솔 창 옆의 Element 매뉴에서 태그를 선택해서 키보드의 delete 키 등을 사용하여 삭제 했다고 해 보자. 그러면 리액트의 상태 변화가 일어나고 태그에 표시될 값을 변경해야 하는데 태그가 사라졌으므로 존재하지 않는 태그에 값을 변경하도록 명령을 내리게 된다. 랜더링 전후의 태그 구조에서 태그를 만드는 것이 아니라 태그의 값만 변경하는 것이므로 리액트는 새로 태그를 생성하지 않고 태그에 값만 변경하라는 명령을 내리기 때문에 사라진 태그는 해당 태그 내부의 값이 리액트의 상태 변화에 의해 변경이 되어도 태그가 새로 생성되지 않고 삭제된 채 남아있게 된다.
<h3style="margin-left: 10px;">current component number : 5</h3>
위 태그를 삭제한 후, prev 버튼 또는 next 버튼을 눌러보자. 태그가 새로 생성되지 않는 것을 알 수 있다.
하지만, 태그를 삭제하지 말고 current component number : 5의 값만 바꾼 후에 prev 버튼이나 next 버튼을 눌러보자. 그러면 current component number : 4의 값으로 다시 원상태로 바뀔 것이다. 이것은 이전의 랜더링 된 current component number : 5의 값에서 다음으로 랜더링 되는 current component number : 4의 값으로 값이 변했기 때문에 리액트의 상태변화가 일어나면서 리액트는 이 태그 내부의 택스트 값을 바꾸기 때문이다. 그런데 수 부분만이 아닌 current component number : 4 택스트 전체가 바뀌었다. 이것은 리액트가 동일한 태그에는 속성 단위로 변경 사항을 인식하기 때문에 속성 전체를 바꾸는 것으로 보면 된다.
AI 바둑 대리기사의 비유
알파고의 등장이후 이를 따라 만든 AI 바둑 프로그램으로 명성을 떨치는 '카타고'와 '절예'라는 프로그램이 있다.
이 두 프로그램 중에서 어느 프로그램이 기량이 더 높은지를 판단하기 위해서 컴퓨터 프로그램을 만들어서 바둑판의 화면에 서로 수를 두도록 만들었다고 하자.
이 바둑을 중계하기 위해 인간 대리기사 2명이 한 명은 카타공의 수를 다른 한 명은 절예의 수를 실제 바둑판에 번갈아가면서 두도록 한다.
경기가 시작이 되고 카타고와 절예가 수를 교환하는 가운데 대리 기사들이 AI를 따라 두는 바둑판에 돌을 떨어뜨려서 바둑판 일부의 돌을 흐뜨려 놓았다.
하지만 AI의 바둑돌 교환은 계속 되면서 바둑 돌을 바로 잡을 시간이 없던 대리 기사들은 AI가 두는 대로 바둑돌을 계속 둔다.
AI는 실물 바둑판의 상황이 어떻든 AI 끼리 수를 교환하는 것에만 관여한다. 따라서 AI의 수 교환이 일어나는 프로그램의 영역에서는 외부의 영향으로 돌의 위치가 바뀌거나 하지 않으며 실물 바둑판의 상황에도 영향을 받지 않는다.
실물 바둑판에서는 일부 돌의 위치가 조금씩 어긋나서 판단이 달리 되어야 하지만, AI끼리의 수 교환이 일어나는 프로그램의 영역에서는 실물 바둑판의 돌의 위치 정보가 반영되어 있지 않기 때문에 AI 영역에서는 바둑 돌을 두는 수는 적절한 판단으로 두어도 실물 바둑판에서의 대리기사에 의한 바둑돌의 위치변화는 이상하게 보이게 되는 것이다.
리액트도 이와 비슷하다. 리액트 내부의 상태관리를 통해서 태그를 랜더링 하는 것은 AI가 바둑을 서로 교환하는 것에 해당하고, 실제 화면에 표시되는 태그는 실물 바둑판에 해당한다. 실물 바둑판이 어떻게 되든 AI는 AI끼리 교환하는 바둑돌의 변화만 감지한다. 마찬가지로 리액트는 실제 브라우저에 랜더링 된 태그가 어떻게 되든 리액트 내부의 이전 랜더링된 태그 구조와 다음으로 랜더링할 태그 구조의 변화만을 감지한다.
리얼돔과 가상돔
리얼돔
돔(DOM)이란 The Document Object Model의 약자로 문서 객체 모델을 의미한다. 브라우저에 HTML 문서가 로딩이 되면 자바스크립트는 HTML 태그를 모두 객체화 한다.
브라우저에서 제공하는 태그 선택 메소드인 document.querySelector('선택할 태그의 CSS 셀렉터') 또는 document.getElementByID('선택할 태그의 아이디') 또는 document.getElementsByClassName('선택할 태그의 클래스 속성 이름')는 브라우저에 로딩된 모든 태그를 구조화 해서 자바스크립트 객체로 만든 문서 객체 모델에서 지정한 태그에 해당하는 객체를 선택하는 것이라고 할 수 있다. 이들 기능을 사용할 때 객체가 새로 만들어지는 것이 아니라 문서 객체 모델에 이미 만들어져 있는 대상을 가져오는 기능이라 할 수 있다.
DOM은 문서 객체 모델이지만, 로딩되는 모든 HTML 태그를 구조화 해서 자바스크립트 객체로 만들어 두기 때문에, 태그의 구조화 된 정보를 가지고 있다. 그래서 태그와 태그 간의 상위 하위 관계, 동일한 부모 태그를 두었을 때 같은 계층에 속한 자식 태그인지 등등 태그간의 위치를 파악할 수 있는 기능이 제공된다. 태그 객체가 Node 타입의 속성을 가지고 있는데 이는 태그 간의 위치 정보를 제공하는 기능을 제공한다.
브라우저에 HTML이 로드될 때 자바스크립트가 HTML의 모든 태그의 구조 정보를 객체화 하는 것을 보통 DOM이라고 부른다. 둥근 형태의 지붕 모양을 돔이라고 부르는데 DOM이 문서 객체 모델의 약자이지만 돔 모양의 형태의 태그 구조로 봐도 된다. 그리고 이를 '리얼 돔'이라고 부른다.
가상돔
'리얼 돔'과 달리 '가상 돔'이란 개념이 있다. 가상돔은 리액트에서 컴포넌트에 의해 반환되는 JSX 태그를 구조화 해서 리액트 라이브러리가 관리하는 것을 의미한다.
리액트의 가상 돔은 JSX의 반환으로 만들어지는 태그 정보만들 가지고 있기 때문에, 브라우저에 로드된 모든 태그의 정보를 구조화 해서 자바스크립트 객체로 가지고 있는 리얼돔과는 다르다.
또한 리액트의 가상돔은 컴포넌트의 JSX가 랜더링 되는 시점의 정보만 가지게 된다. 따라서 컴포넌트 함수가 가장 처음 반환하는 JSX 태그의 구조 정보 + 컴포넌트 함수가 재실행되면서 반환하는 JSX 태그 구조에서 재실행 되기 이전에 반환한 JSX의 태그 구조 정보에서 변경된 데이터만을 갱신한 JSX 태그 구조 정보를 갖게 된다.
간단히 말해서 가상돔은 초기 랜더링 때의 JSX 태그 구조와 이후 컴포넌트의 반환되는 JSX 태그의 변경점에 대한 태그 정보만 갱신한 태그 구조 정보를 가지고 있으며, 컴포넌트 함수에 의해 JSX의 변경사항이 없는 경우 리액트의 가상돔은 아무런 태그 정보도 업데이트 하지 않는다.
가상돔은 리액트에서 컴포넌트 함수의 반환되는 JSX의 변경점을 감지하기 위해 내부적으로 다뤄지는 로직이며, 가상돔의 변경사항은 리얼돔에 반영이 되지만, 리얼돔의 변경사항은 가상돔에 반영되지 않는 특성을 가지고 있다.
앞선 비유에서 AI끼리 바둑을 두는 것은 가상돔의 개념에 해당하고, 대리 기사들이 실제 바둑판에 AI가 둔 바둑을 그대로 따라두는 것이 리얼돔에 해당한다고 보면 된다. 가상돔을 변경하면 리얼돔도 변경이 되지만, 리얼돔을 변경한다고 해서 가상돔이 변경되는 것은 아니다.
리액트에서 리얼돔을 변경하는 것은 안티패턴
리액트의 핵심은 상태 변화에 따라서 JSX를 새로 만들고 변경된 JSX 태그를 화면에 랜더링하는 것이다.
이 때 리액트는 따로 리얼돔의 변화를 감지하는 로직을 만들지 않는 이상 기본적으로 제공되는 기능으로는 리얼돔의 변화를 감지할 수 없으며, 리액트의 가상돔의 생성과 변화만을 추적하여 가상돔의 구조만 랜더링하는 방식을 사용한다.
리액트를 사용하는데 리얼돔을 변경하는 것은 리얼돔을 직접 변경한 대상은 리액트의 상태 변화만으로는 충분히 다룰 수 없는 대상이 되는 것을 의미한다. 리액트는 직접 변경한 리얼돔의 정보를 가상돔에 반영하지 않기 때문에 리액트의 JSX가 반환하는 태그 구조와 화면에 표시되는 태그 간의 불일치를 초래한다. 따라서 리얼돔에 직접적인 변경을 가하게 되면 변경된 태그의 변화를 이해하기 위해서는 리액트에서의 태그의 변화 뿐만 아니라 리얼돔을 변경사항을 고려해야 한다.
리액트를 사용하는 것은 실제 브라우저 화면에 리액트의 컴포넌트 함수에서 반환하는 JSX를 태그가 생성하는 태그 구성을 그대로 적용해야 하는데, 리얼돔을 직접 변경하게 되면 리액트에서 생성한 태그와 실제 브라우저의 태그 간의 불일치를 만들어 내기 때문에, 리액트에서 의도하지 않은 동작이 일어나게 된다. 화면에 랜더링 되는 태그는 리액트의 상태 변화와 JSX만으로 이해될 수 있어야 하는데, 리얼돔을 직접 변경하면, 리얼돔을 직접 변경한 로직도 생각해서 리액트의 코드를 짜 줘야 한다.
리액트를 사용하는 이유는 리얼돔을 사용할 때 하나의 태그가 변경 되었을 때 연관되는 다른 태그의 변경 사항을 자동으로 변경하기 위해서이다. 리액트를 사용하지 않고 직접 태그 하나하나를 컨트롤하던 방식을 사용하면 한 태그에서의 동작이 일어날 때 다른 태그에서의 동작도 바꾸는 코드를 함께 추가해 줘야 했지만, 리액트를 사용하면 하나의 컴포넌트에서 상태를 변화시켰을 때 다른 컴포넌트의 상태도 변경될 수 있어서 코드의 복잡성이 감소하기 때문에 사용하는 것이다. 만약 리얼돔을 변경해서 리액트만으로 태그의 변경사항을 이해하지 못하고 리얼돔의 변경 코드까지 알고 코드를 만들어야 한다면 복잡성을 줄이기 위해 도입되는 리액트의 복잡성이 오히려 늘어나기 때문에 리액트를 사용할 때, 리얼돔을 직접 변경하는 것은 안티패턴에 해당한다.
예를 들어 장바구니 아이콘과 장바구니 리스트가 있다. 장바구니 리스트에서 한 종류의 아이템을 지우면 장바구니 아이콘에서 빨간색으로 작게 표시된 숫자는 1 감소해야 하는 화면이 있다고 생각해 보자. 장바구니 아이콘의 표시 숫자는 아이템 리스트를 다루는 상태 변수를 통해서 상태 변수에 있는 아이템의 종류에 따라서 계산이 되어 표시되는 로직이라고 하자. 장바구니 리스트에서 한 종류의 아이템을 삭제하면 아이템 리스트에 관한 정보를 가지고 있는 상태 값에서 해당 아이템의 정보가 사라지게 되고 이 상태 변수는 장바구니 리스트 뿐만 아니라 장바구니 아이콘의 빨간 동그라미 안의 하얀 숫자 표기 부분에도 영향을 주기 때문에 함께 변경이 된다. 그런데 만약 리액트와 같은 라이브러리를 쓰지 않는다면 장바구니 리스트의 태그를 변경하는 코드를 작성하면서 장바구니 아이콘의 숫자도 바꿔 주는 코드를 작성해야 한다. 리액트를 사용할 때는 상태 변수가 변경되면 같은 상태 변수를 사용하는 다른 태그의 값도 자동으로 변경되지만, 리액트를 사용하지 않고 직접 태그를 변경하게 되면 한 부분의 변경과 함께 변경되는 모든 태그를 각각 변경 시키는 코드를 추가 해 주어야 한다.
useRef를 사용해서 리얼돔의 태그 다루기
자바스크립트에서는 리얼돔 내부의 태그를 선택할 수 있는 기능을 제공하지만, 리액트는 리액트 내부에서 다뤄지는 가상돔을 선택할 수 있는 기능을 제공하지 않는다. 컴포넌트 함수를 재실행 하면서 반환되는 JSX로 가상돔의 구조를 바꿀 수도 있고, 가상 돔 내의 특정 태그를 변경한다던가 특정 태그의 속성을 변경한다던가 할 수 있지만, 컴포넌트 함수를 다시 실행하여 JSX를 반환하지 않는다면 가상돔의 특정 태그를 변경할 수 없다.
리액트에서는 JSX에서 랜더링 되는 특정 태그의 가상돔을 직접 다룰 수 있는 기능을 제공하지는 않지만, 해당 태그의 리얼돔을 직접 다룰 수 있는 기능을 제공한다. 이를 위해서는 useRef로 생성된 객체를 JSX 태그의 Ref속성으로 지정하는 코드가 필요하다.
위의 코드는 <input type='number' style={style.input} ref={inputTagRef}></input>라는 태그는 inputTagRef.current로 접근이 가능하게 된다.
useRef 함수의 반환을 받은 inputTagRef 변수는 처음에는 {current: undefined}의 객체를 참조하지만, 컴포넌트 함수가 처음 JSX를 반환한 이후에 JSX의 태그에서 Ref 속성으로 지정한 객체는 컴포넌트 함수가 다시 실행 될 때 useRef의 반환 받은 값에 세팅이 되면서 inputTagRef 변수는 {current: input}라는 객체를 참조하게 된다. 이 때, input 태그는 가상돔이 아닌 리얼돔의 태그 객체를 가리킨다.
리얼돔의 태그 객체란 document.querySelector('선택할 태그의 CSS 셀렉터') 또는 document.getElementByID('선택할 태그의 아이디') 등으로 선택한 것과 같은 종류의 객체를 갖는다는 것이다. <input type='number' style={style.input} ref={inputTagRef}></input>란 태그를 선택한 객체를 {current: input}의 input 부분에서 참조하고 있는 형태의 값으로 반환하기 때문에 태그 객체에 접근하기 위해서는 inputTagRef.current와 같은 방식으로 사용한다.
리액트의 태그는 컴포넌트 함수를 다시 실행할 때 마다 useRef()로 반환되는 객체에서 참조로 접근할 수 있으며, 이때 이전에 JSX 태그가 렌더링 될 때 태그의 Ref 속성에 useRef()의 반환 값을 할당한 태그가 다음의 useRef()에서 반환되게 된다. 따라서 동일한 useRef()의 반환 값을 담는 변수인 inputTagRef을 Ref 속성으로 지정했다고 하더라도, JSX에서 반환되는 태그가 달라진다면 리액트의 상태가 변할 때 다른 태그를 useRef()가 반환하게 되고 inputTagRef는 이전의 태그와 다른 태그를 참조하게 된다.