Vanilla JS에서 React 및 JSX 에뮬레이션

게시 됨: 2022-03-11

프레임워크를 싫어하는 사람은 거의 없지만, 그 중 하나라도 삶을 조금 더 쉽게 만들어 주는 기능을 메모하고 채택해야 합니다.

나는 과거에 프레임워크를 사용하는 것에 반대했습니다. 그러나 최근에는 일부 프로젝트에서 React 및 Angular로 작업한 경험이 있습니다. 처음 몇 번은 코드 편집기를 열고 Angular로 코드를 작성하기 시작했는데 이상하고 부자연스러웠습니다. 특히 프레임워크를 사용하지 않고 10년 이상 코딩한 후. 잠시 후 나는 이러한 기술을 배우기로 결심했습니다. 매우 빠르게 한 가지 큰 차이점이 분명해졌습니다. DOM을 조작하기가 너무 쉬웠고 필요할 때 노드의 순서를 조정하기가 쉬웠으며 UI를 빌드하는 데 페이지와 코드 페이지가 필요하지 않았습니다.

나는 여전히 프레임워크나 아키텍처에 얽매이지 않는 자유를 선호하지만 DOM 요소를 하나로 만드는 것이 훨씬 더 편리하다는 사실을 무시할 수 없었습니다. 그래서 나는 바닐라 JS에서 경험을 에뮬레이트하는 방법을 찾기 시작했습니다. 제 목표는 React에서 이러한 아이디어 중 일부를 추출하고 동일한 원칙을 일반 JavaScript(종종 바닐라 JS라고도 함)에서 구현하여 개발자의 삶을 조금 더 쉽게 만드는 방법을 보여주는 것입니다. 이를 수행하기 위해 GitHub 프로젝트 탐색을 위한 간단한 앱을 빌드해 보겠습니다.

간단한 GitHub 검색 앱

우리가 만들고 있는 앱.

JavaScript를 사용하여 프론트 엔드를 구축하는 방법에 관계없이 DOM에 액세스하고 조작할 것입니다. 우리 애플리케이션의 경우 각 저장소의 표현(썸네일, 이름 및 설명)을 구성하고 DOM에 목록 요소로 추가해야 합니다. GitHub Search API를 사용하여 결과를 가져올 것입니다. 그리고 JavaScript에 대해 이야기하고 있으므로 JavaScript 저장소를 검색해 보겠습니다. API를 쿼리하면 다음 JSON 응답을 받습니다.

 { "total_count": 398819, "incomplete_results": false, "items": [ { "id": 28457823, "name": "freeCodeCamp", "full_name": "freeCodeCamp/freeCodeCamp", "owner": { "login": "freeCodeCamp", "id": 9892522, "avatar_url": "https://avatars0.githubusercontent.com/u/9892522?v=4", "gravatar_id": "", "url": "https://api.github.com/users/freeCodeCamp", "site_admin": false }, "private": false, "html_url": "https://github.com/freeCodeCamp/freeCodeCamp", "description": "The https://freeCodeCamp.org open source codebase "+ "and curriculum. Learn to code and help nonprofits.", // more omitted information }, //... ] }

리액트의 접근 방식

React는 HTML 요소를 페이지에 작성하는 것을 매우 간단하게 만들고 순수 JavaScript로 구성 요소를 작성하는 동안 항상 갖고 싶었던 기능 중 하나입니다. React는 일반 HTML과 매우 유사한 JSX를 사용합니다.

그러나 그것은 브라우저가 읽는 것이 아닙니다.

내부적으로 React는 JSX를 React.createElement 함수에 대한 호출로 변환합니다. GitHub API의 한 항목을 사용하는 JSX의 예를 살펴보고 그것이 무엇으로 번역되는지 봅시다.

 <div className="repository"> <div>{item.name}</div> <p>{item.description}</p> <img src={item.owner.avatar_url} /> </div>;
 ; React.createElement( "div", { className: "repository" }, React.createElement( "div", null, item.name ), React.createElement( "p", null, item.description ), React.createElement( "img", { src: item.owner.avatar_url } ) );

JSX는 매우 간단합니다. 일반 HTML 코드를 작성하고 중괄호를 추가하여 개체에서 데이터를 삽입합니다. 괄호 안의 JavaScript 코드가 실행되고 결과 DOM에 값이 삽입됩니다. JSX의 장점 중 하나는 React가 변경 및 업데이트를 추적하기 위해 가상 DOM(페이지의 가상 표현)을 생성한다는 것입니다. 전체 HTML을 다시 작성하는 대신 React는 정보가 업데이트될 때마다 페이지의 DOM을 수정합니다. 이것은 React가 해결하기 위해 만든 주요 문제 중 하나입니다.

jQuery 접근 방식

개발자들은 jQuery를 많이 사용했습니다. 여전히 인기가 있고 순수 JavaScript의 솔루션에 매우 가깝기 때문에 여기에서 언급하고 싶습니다. jQuery는 DOM을 쿼리하여 DOM 노드(또는 DOM 노드 모음)에 대한 참조를 얻습니다. 또한 내용을 수정하기 위한 다양한 기능으로 해당 참조를 래핑합니다.

jQuery에는 자체 DOM 구성 도구가 있지만 실제로 가장 자주 보는 것은 HTML 연결입니다. 예를 들어 html() 함수를 호출하여 선택한 노드에 HTML 코드를 삽입할 수 있습니다. jQuery 문서에 따르면 demo-container 클래스로 div 노드의 내용을 변경하려면 다음과 같이 할 수 있습니다.

 $( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );

이 접근 방식을 사용하면 DOM 요소를 쉽게 만들 수 있습니다. 그러나 노드를 업데이트해야 할 때 필요한 노드를 쿼리하거나 (더 일반적으로) 업데이트가 필요할 때마다 전체 스니펫을 다시 만드는 것으로 대체해야 합니다.

DOM API 접근 방식

브라우저의 JavaScript에는 페이지의 노드 생성, 수정 및 삭제에 직접 액세스할 수 있는 DOM API가 내장되어 있습니다. 이는 React의 접근 방식에 반영되었으며 DOM API를 사용하여 해당 접근 방식의 이점에 한 걸음 더 다가갔습니다. 실제로 변경이 필요한 페이지 요소만 수정하고 있습니다. 그러나 React는 별도의 가상 DOM도 추적합니다. 가상 DOM과 실제 DOM의 차이점을 비교함으로써 React는 수정이 필요한 부분을 식별할 수 있습니다.

이러한 추가 단계는 때때로 유용하지만 항상 그런 것은 아니며 DOM을 직접 조작하는 것이 더 효율적일 수 있습니다. 생성된 노드에 대한 참조를 반환하는 _document.createElement_ 함수를 사용하여 새 DOM 노드를 생성할 수 있습니다. 이러한 참조를 추적하면 업데이트해야 하는 부품이 포함된 노드만 쉽게 수정할 수 있습니다.

JSX 예제와 동일한 구조와 데이터 소스를 사용하여 다음과 같은 방식으로 DOM을 구성할 수 있습니다.

 var item = document.createElement('div'); item.className = 'repository'; var nameNode = document.createElement('div'); nameNode.innerHTML = item.name item.appendChild(nameNode); var description = document.createElement('p'); description.innerHTML = item.description; item.appendChild(description ); var image = new Image(); Image.src = item.owner.avatar_url; item.appendChild(image); document.body.appendChild(item);

코드 실행의 효율성만 염두에 두고 있다면 이 접근 방식이 매우 좋습니다. 그러나 효율성은 실행 속도뿐만 아니라 유지 관리의 용이성, 확장성 및 가소성으로 측정됩니다. 이 접근 방식의 문제는 매우 장황하고 때로는 복잡하다는 것입니다. 기본 구조를 구성하는 것만으로도 많은 함수 호출을 작성해야 합니다. 두 번째로 큰 단점은 생성되고 추적되는 변수의 수입니다. 작업 중인 구성 요소에 30개의 DOM 요소가 포함되어 있다고 가정해 보겠습니다. 30개의 다른 DOM 요소와 변수를 만들고 사용해야 합니다. 그것들 중 일부를 재사용하고 유지 보수성과 가소성을 희생시키면서 약간의 저글링을 할 수 있지만 정말 빨리 지저분해질 수 있습니다.

또 다른 중요한 단점은 작성해야 하는 코드 라인의 수 때문입니다. 시간이 지남에 따라 한 부모에서 다른 부모로 요소를 이동하는 것이 점점 더 어려워집니다. 제가 React에서 정말 감사하게 생각하는 것 중 하나입니다. JSX 구문을 보고 몇 초 안에 포함된 노드, 위치 및 필요한 경우 변경할 수 있습니다. 그리고 처음에는 별 것 아닌 것처럼 보일 수도 있지만 대부분의 프로젝트에는 더 나은 방법을 찾도록 하는 지속적인 변경 사항이 있습니다.

제안 된 해법

DOM으로 직접 작업하면 작업이 완료되지만 특히 HTML 속성과 중첩 노드를 추가해야 하는 경우 페이지 구성을 매우 장황하게 만듭니다. 따라서 아이디어는 JSX와 같은 기술로 작업할 때의 이점을 포착하고 우리의 삶을 더 단순하게 만드는 것입니다. 복제하려는 이점은 다음과 같습니다.

  1. DOM 요소 생성이 읽고 수정하기 쉽도록 HTML 구문으로 코드를 작성하십시오.
  2. React의 경우처럼 가상 DOM과 동등한 것을 사용하지 않기 때문에 관심 있는 노드를 표시하고 추적할 수 있는 쉬운 방법이 필요합니다.

다음은 HTML 스니펫을 사용하여 이를 수행하는 간단한 기능입니다.

 Browser.DOM = function (html, scope) { // Creates empty node and injects html string using .innerHTML // in case the variable isn't a string we assume is already a node var node; if (html.constructor === String) { var node = document.createElement('div'); node.innerHTML = html; } else { node = html; } // Creates of uses and object to which we will create variables // that will point to the created nodes var _scope = scope || {}; // Recursive function that will read every node and when a node // contains the var attribute add a reference in the scope object function toScope(node, scope) { var children = node.children; for (var iChild = 0; iChild < children.length; iChild++) { if (children[iChild].getAttribute('var')) { var names = children[iChild].getAttribute('var').split('.'); var obj = scope; while (names.length > 0) { var _property = names.shift(); if (names.length == 0) { obj[_property] = children[iChild]; } else { if (!obj.hasOwnProperty(_property)){ obj[_property] = {}; } obj = obj[_property]; } } } toScope(children[iChild], scope); } } toScope(node, _scope); if (html.constructor != String) { return html; } // If the node in the highest hierarchy is one return it if (node.childNodes.length == 1) { // if a scope to add node variables is not set // attach the object we created into the highest hierarchy node // by adding the nodes property. if (!scope) { node.childNodes[0].nodes = _scope; } return node.childNodes[0]; } // if the node in highest hierarchy is more than one return a fragment var fragment = document.createDocumentFragment(); var children = node.childNodes; // add notes into DocumentFragment while (children.length > 0) { if (fragment.append){ fragment.append(children[0]); }else{ fragment.appendChild(children[0]); } } fragment.nodes = _scope; return fragment; }

아이디어는 간단하지만 강력합니다. 생성하려는 HTML 함수를 문자열로 보내고 HTML 문자열에서 참조를 생성하려는 노드에 var 속성을 추가합니다. 두 번째 매개변수는 해당 참조가 저장될 객체입니다. 지정하지 않으면 반환된 노드 또는 문서 조각에 "노드" 속성을 생성합니다(가장 높은 계층 노드가 둘 이상인 경우). 모든 것이 60줄 미만의 코드로 수행됩니다.

이 기능은 세 단계로 작동합니다.

  1. 새 빈 노드를 만들고 새 노드에서 innerHTML을 사용하여 전체 DOM 구조를 만듭니다.
  2. 노드를 반복하고 var 속성이 있으면 해당 속성이 있는 노드를 가리키는 범위 개체에 속성을 추가합니다.
  3. 계층 구조의 최상위 노드를 반환하거나 둘 이상의 경우 문서 조각을 반환합니다.

이제 예제를 렌더링하기 위한 코드가 어떻게 생겼습니까?

 var UI = {}; var template = ''; template += '<div class="repository">' template += ' <div var="name"></div>'; template += ' <p var="text"></p>' template += ' <img var="image"/>' template += '</div>'; var item = Browser.DOM(template, UI); UI.name.innerHTML = data.name; UI.text.innerHTML = data.description; UI.image.src = data.owner.avatar_url;

먼저 생성된 노드에 대한 참조를 저장할 개체(UI)를 정의합니다. 그런 다음 사용할 HTML 템플릿을 문자열로 작성하여 대상 노드를 "var" 속성으로 표시합니다. 그런 다음 템플릿과 참조를 저장할 빈 개체를 사용하여 Browser.DOM 함수를 호출합니다. 마지막으로 저장된 참조를 사용하여 노드 내부에 데이터를 배치합니다.

이 접근 방식은 또한 DOM 구조 구축과 데이터 삽입을 별도의 단계로 분리하여 코드를 체계적이고 구조적으로 유지하는 데 도움이 됩니다. 이를 통해 DOM 구조를 별도로 생성하고 데이터가 사용 가능해지면 데이터를 채우거나 업데이트할 수 있습니다.

결론

우리 중 일부는 프레임워크로 전환하고 통제권을 넘겨주는 아이디어를 싫어하지만 이러한 프레임워크가 가져오는 이점을 인식하는 것이 중요합니다. 그들이 그렇게 인기있는 이유가 있습니다.

프레임워크가 항상 귀하의 스타일이나 요구 사항에 맞지는 않을 수 있지만 일부 기능과 기술은 프레임워크에서 채택, 에뮬레이트 또는 때로는 분리될 수 있습니다. 일부는 번역에서 항상 손실되지만 프레임워크가 부담하는 비용의 아주 작은 부분으로 많은 것을 얻고 사용할 수 있습니다.