리액트의 장점
편리한 반응형 렌더링
단일 페이지 애플리케이션(SPA, Single Page Application)의 개념이 등장하기 한참 전의 초기 웹 개발 시대에는 사용자가 페이지에서 수행하는 모든 상호작용(예: 버튼 클릭)을 처리하기 위해 사용자가 현재 보고 있는 것과 사소한 차이점이 있는 경우에도 완전히 새로운 페이지를 서버로부터 전송해야 했습니다. 이 방식은 사용자 관점에서는 형편없는 경험을 제공했지만 개발자는 특정 상호작용이나 특정 시점에 사용자에게 보여줄 내용을 아주 쉽게 계획할 수 있습니다.
단일 페이지 애플리케이션은 사용자가 상호작용하는 동안 끊임없이 새로운 데이터를 가져오고 DOM의 일부를 변환합니다. 그런데 인터페이스가 복잡해지면 애플리케이션의 현재 상태를 조사하고 시기적절하게 DOM에 변경사항을 반영하고 업데이트하는 작업이 더욱 복잡해집니다.
여러 자바스크립트 프레임워크가 (특히 리액트가 등장하기 전에) 증가하는 복잡성을 해결하고 인터페이스와 상태를 동기화하기 위해 데이터 바인딩(Data binding)을 이용하기도 했지만 이 기법은 유지 관리성, 확장성, 성능 면에서 단점이 있습니다.
반응형 렌더링(reactive rendering)은 기존 데이터 바인딩보다 사용하기가 쉽습니다. 개발자가 컴포넌트의 모양과 동작을 선언식으로 정의하면 리액트가 데이터 변경을 감지하고 개념상으로 전체 인터페이스를 다시 렌더링합니다.
그런데 상태 데이터가 변경될 때마다 실제로 전체 인터페이스를 다시 렌더링하는 것은 성능 저하를 감안할 때 불가능하므로 리액트는 “가상 DOM”이라고 하는 DOM의 In-Memory 경량 표현 을 이용해 작업합니다.
실제 DOM을 조작하는 것보다는 DOM의 In-Memory 표현을 조작하는 것이 훨씬 효율적이고 빠릅니다. 예를 들어, 사용자 상호작용이나 데이터 가져오기 등을 통해 애플리케이션의 상태가 달라지면 리액트는 신속하게 UI의 현재 상태와 원하는 상태를 비교하고 실제 DOM의 최소 집합 으로 계산을 수행합니다. 덕분에 리액트는 아주 빠르고 효율적으로 작업할 수 있습니다. 리액트 앱은 모바일 장치에서도 손쉽게 60fps 속도를 달성할 수 있습니다.
순수 자바스크립트를 이용한 컴포넌트 기반 개발
리액트 애플리케이션의 모든 부분은 특정 목적을 가진 독립형 구성요소인 컴포넌트로 구성됩니다. 컴포넌트를 이용해 애플리케이션을 개발하면 “분할 정복” 방식을 적용해서 여러 작은 컴포넌트를 조합해 복잡하고 기능이 많은 컴포넌트를 쉽게 만들 수 있습니다.
리액트 컴포넌트는 기존 웹 애플리케이션 UI에 많이 이용되던 HTML 지시문이나 템프릿 언어가 아닌 순수 자바스크립트 로 작성합니다. 템플릿은 UI를 구축하는 데 이용할 수 있는 전체 추상화 집합을 좌우하므로 한계로 작용하는 단점이 있습니다. 따라서, 리액트는 완전한 기능을 갖춘 프로그래밍 언어를 이용하므로 추상화를 구축하는 데 큰 장점이 있습니다.
Hello, React!
앞에서 컴포넌트는 리액트 UI의 기본 구성요소라고 했습니다. 기본적으로 리액트 컴포넌트는 다음과 같이 컴포넌트의 UI에 대한 설명을 반환하는 render 메서드가 포함된 자바스크립트 클래스입니다.
class Hello extends React.Component {
render() {
return (
<h1>Hello World</h1>
);
}
}
자바스크립트 코드 안에 HTML 태그가 있는 것을 볼 수 있습니다. 앞에서 언급한 것처럼 리액트는 코드 안에 인라인으로 XML(결과적으로 HTML까지)을 작성할 수 있게 해주는 JSX라는 자바스크립트 구문 확장이 있습니다.
JSX는 필수는 아니지만 선언식 구문을 이용할 수 있고 표현성이 좋으며 자바스크립트 함수 호출로 변환 되므로 언어의 의미를 바꾸지 않는다는 장점 때문에 리액트 컴포넌트에서 UI를 정의하는 표준적인 방법으로 널리 이용되고 있습니다.
ReactJS Development Workflow
예전에는 모든 자바스크립트를 한 파일레 작성하고, 자바스크립트 라이브러리 한두개를 직접 내려받은 다음, 모든 것을 한 페이지에 집어넣던 시절이 있었습니다. 물론 지금도 리액트 라이브러리를 축소된 자바스크립트 파일로 내려받은 다음, 런타임에 JSX를 변환하면서 컴포넌트를 실행하는 것도 가능하지만, 아주 작은 데모나 프로토타입이 아니면 아무도 이렇게 하지 않습니다.
가장 기본적인 시나리오에서도 다음과 같은 작업이 가능한 개발 워크플로우가 필요합니다.
- JSX를 작성하고 즉석에서 일반 자바스크립트로 변환
- 코드를 모듈 패턴으로 작성
- 의존성 관리
- 자바스크립트 파일을 번들로 만들고 디버깅을 위해 소스 맵을 이용
이를 위해 리액트 프로젝트의 기본 프로젝트 구조에는 다음과 같은 항목이 포함됩니다.
- 모든 자바스크립트 모듈을 포함하는 소스 폴더.
- index.html 파일. 이 페이지는 애플리케이션의 자바스크립트를 로드하고, 리액트에서 컴포넌트를 렌더링하는 데 이용하는 div를 제공하는 역할을 합니다.
- package.json 파일. 이는 표준 npm 매니페스트 파일입니다. 의존성을 지정하고 스크립트 작성을 정의하는 데 이용됩니다.
- 모듈 패키저 또는 빌드 도구. JSX 변환과 모듈/의존성 번들 작업에 이용됩니다. Grunt, GUlp, Brunch 등이 있지만, 리액트 커뮤니티에서는 주로 Webpack을 선호합니다.
빠르게 시작하기
소스 코드는 깃허브 페이지에서 확인하실 수 있습니다. 모든 의존성을 설치하려면 터미널에서 npm install
을 실행하고, 개발 서버를 실행하려면 npm start
를 입력하면 됩니다. 결과는 http://localhost:8080/ 에 접속해서 확인할 수 있습니다.
동적 값
JSX에서 중괄호({}) 안에 있는 값은 자바스크립트 식으로 계산되고 마크업 안에 렌더링된다. 로컬 변수의 값을 렌더링하려면 다음 예제와 같이 할 수 있다.
import React, { Component } from 'react';
class Hello extends Component {
render() {
var place = "World";
return (
<h1>Hello {place}</h1>
);
}
}
React.render(<Hello />, document.getElementById("root"));
가상 DOM의 동작 방식
리액트 설계의 핵심적인 측면 중 하나는 업데이트가 수행될 때마다 모든 것을 다시 렌더링하는 것처럼 API가 구성됐다는 점입니다. DOM 조작은 여러 가지 이유로 속도가 느리므로 리액트는 성능을 개선하기 위해 가상 DOM을 구현합니다. 리액트는 애플리케이션의 상태가 바뀔 때마다 실제 DOM을 업데이트하는 대신 원하는 DOM 상태가 비슷한 가상 트리를 생성 합니다. 그런 다음 전체 DOM 모드를 다시 생성하지 않고도 실제 DOM을 가상 DOM과 같이 만드는 방법 을 알아냅니다.
가상 DOM 트리와 실제 DOM 트리를 동일하게 만드는 데 필요한 최소 변경 횟수를 알아내는 프로세스를 조정 reconciliation 이라고 하며, 일반적으로 이 작업은 아주 복잡하고 실행 비용이 높습니다. 이러한 조정 작업은 여러 차례에 걸쳐 반복과 최적화를 거친 후에도 매우 까다롭고 시간을 많이 소비합니다. 리액트는 이 작업을 조금이라도 수월하게 하고 훨씬 빠르고 실용적인 알고리즘을 적용하기 위해 일반적인 애플리케이션의 작동 방법에 대해 몇 가지 사항을 가정합니다.
- DOM 트리의 노드를 비교할 때 노드가 다른 유형일 경우(예: div를 span으로 변경) 리액트는 이를 서로 다른 하위 트리로 취급해 첫 번째 항목을 버리고 두 번째 항목을 생성/삽입합니다.
- 커스텀 컴포넌트에도 동일한 논리를 적용합니다. 컴포넌트가 동일한 유형이 아닌 경우 리액트는 컴포넌트가 렌더링하는 내용을 비교조차 하지 않고 DOM에서 첫 번째 항목을 제거한 후 두 번째 항목을 삽입합니다.
- 노드가 같은 유형인 경우 리액트는 둘 중 한 가지 방법으로 이를 처리합니다.
- DOM 요소의 경우(예: <div id="before" />를 <div id="after" />로 변경) 리액트는 특성과 스타일만 변경합니다(요소 트리는 대체하지 않음).
- 커스텀 컴포넌트의 경우(예: <Contact details={false} />를 <Contact details={true} />로 변경) 리액트는 컴포넌트를 대체하지 않고 새로운 속성을 현재 마운팅된 컴포넌트로 전달합니다. 그러면 이 컴포넌트에서 새로 render()가 트리거되고 새로운 결과를 이용한 프로세스가 다시 시작됩니다.