블로그 리스트
이 블로그 포스트는 시리즈로 제작되었습니다. 다음은 create-react-app
의 시리즈 리스트입니다.
- React란
- create-react-app
- create-react-app에서 TypeScript
- [타입스크립트] create-react-app에서 절대 경로 import
- create-react-app에서 styled-components
- Jest
- create-react-app에서 react-testing-library로 테스트하기
개요
이번 블로그 포스트에서는 리액트가 무엇인지 간단하게 살펴보고, 리액트의 특징에 대해 살펴보려고 합니다.
리액트란
리액트는 UI 자바스크립트 라이브러리로써 싱글 페이지 애플리케이션의 UI(User Interface)를 생성하는데 집중한 라이브러리입니다. 리액트는 자바스크립트에 HTML을 포함하는 JSX(JavaScript XML)이라는 간단한 문법과 단방향 데이터 바인딩(One-way Data Binding)을 사용하고 있습니다. 그리고 가상 돔(Virtual DOM)이라는 개념을 사용하여 웹 애플리케이션의 퍼포먼스를 최적화한 라이브러리입니다.
리액트는 싱글 페이지 애플리케이션에서 UI를 만드는 자바스크립트 라이브러리이다보니, 싱글 페이지 애플리케이션 제작을 하는 다른 프레임워크에 비해 부족한 부분이 있습니다. 예를 들어 리액트는 페이지 전환 기능을 제공하지 않기 때문에, 리액트를 사용하여 페이지 전환을 해야한다면, react-router와 같은 추가적인 라이브러리를 사용해야 합니다.
리액트의 특징
리액트는 가상 돔, 단방향 데이터 바인딩 등 여러 특징을 가지고 있습니다. 여기서는 이런 특징들에 관해 자세히 살펴보도록 합시다.
가상 돔
리액트는 가상 돔을 사용하여 웹 애플리케이션의 성능을 극대화 시켰습니다. 가상 돔을 이해하기 위해서는 우선 HTML과 CSS가 렌더링(Rendering) 되는 과정을 이해할 필요가 있습니다.
웹 브라우저가 네트워크를 통해 HTML을 전달 받으면 브라우저의 렌더링 엔진은 HTML을 파싱하고 돔 노드(DOM Node)로 이루어진 트리를 만듭니다. 또한 CSS 파일과 HTML의 요소들(Element)의 인라인 스타일을 파싱하여 스타일 정보를 가진 스타일 트리도 생성합니다.
이렇게 렌더 트리가 완성되면 브라우저는 Attachment라는 과정을 통해 스타일 정보를 계산합니다. 렌더 트리로 생성된 모든 노드들은 attach라는 함수를 가지고 있는데, 이 Attachment라는 과정에서 이 메소드들이 호출되게 되며, 해당 메소드는 스타일 정보를 계산하고 결과값을 객체 형태로 반환하게 됩니다.
이때 이 계산 과정은 모두 동기적(Synchronous)으로 동작하게 되며, 만약 렌더 트리에 새로운 노드가 추가된다면 해당 노드의 attach 메소드가 실행되어 계산 과정을 거치게 됩니다.
렌더 트리는 Attachment 과정을 거친 후 레이아웃이라는 과정을 거치게 됩니다. 이 레이아웃 과정에서는 브라우저가 렌더 트리의 각 노드들에 좌표를 부여하고 정확히 어디에 어떻게 표시되는지를 결정합니다.
마지막으로 브라우저는 페인팅(Painting)이라는 과정을 거치게 됩니다. 이 페인팅 과정에서는 각 노드들에 paint 함수를 호출하여 렌더링된 요소들에 색상을 입히게 됩니다.
이런 과정을 거쳐 표시된 HTML을 자바스크립트를 사용하여 DOM을 조작하게 되면 각 노드의 좌표를 계산하기 위해 레이아웃 과정이 다시 실행되고, 그 이후 색상을 입히기 위한 페인팅 과정이 다시 진행되게 됩니다. 이렇게 DOM 조작으로 레이아웃 과정이 다시 진행되는 것을 리플로우(Reflow)라고 하며, 페인팅 과정이 다시 진행되는 것을 리페인트(Repaint)라고 합니다. 이 리플로우와 리페인트는 DOM의 각 노드에 대한 연산 과정을 다시 수행함으로 이 과정이 많이 수행될수록 웹 서비스의 성능이 저하되는 문제가 발생하게 됩니다.
리액트는 이 리플로우와 리페인트가 자주 수행되는 문제를 해결하기 위해 화면에 표시되는 DOM과 동일한 DOM을 메모리상에 만들고 DOM 조작이 발생하면 메모리상에 생성한 가상 돔에 모든 연산을 수행한 후, 실제 DOM을 갱신하여 리플로우/리페인트의 연산을 최소화하였습니다.
예를 들어 사용자가 Todo 리스트 앱을 사용할 때, 아무것도 없던 리스트에 할일을 하나 추가하는 과정을 생각해 봅시다. 할일을 입력하고 추가 버튼을 누르면, 아무것도 없던 리스트에 할일을 하나 표시하고, 전체 할일 개수를 표시합니다. 이 때, 가상 돔이 없는 경우, 할일을 하나 표시하기 위해 리플로우/리페인트가 한 번 발생하고, 전체 할일 개수를 표시하기 위해 리플로우/리페인트가 다시 한 번 발생하게 됩니다.
하지만, 리액트에서는 이 모든 과정을 가상 돔에서 수행합니다. 따라서 사용자가 할일을 입력하고 추가 버튼을 누르면, 할일 표시, 전체 할일 개수를 표시하기 위해 가상 돔에서 계산을 한 후, 계산 결과를 브라우저에 전달하여 리플로우/리페인트를 한 번만 수행하도록 합니다.
이렇게 리액트에서는 가상 돔을 사용하여 DOM이 자주 갱신되는 싱글 페이지 애플리케이션의 리플로우와 리페인트를 최소화함으로써 성능을 최적화시켰습니다.
단방향 데이터 바인딩
단방향 데이터 바인딩을 알아보기전에 양방향 데이터 바인딩에 대해서 잠시 살펴봅시다. 양방향 데이터 바인딩은 사용자 UI의 데이터 변경을 감시하는 Watcher와 자바스크립트 데이터의 변경을 감시하는 Watcher가 UI와 자바스크립트 데이터를 자동으로 동기화 시켜주는 방식을 말합니다. 이를 통해 프로그래머는 자바스크립트내에 데이터 변경과 사용자 UI에 데이터 변경에 관한 동기화를 신경쓰지 않고 프로그램을 작성할 수 있습니다.
하지만, 하나에 데이터 동기화에 두 개의 Watcher가 사용되고, 데이터 많아지게 되면 이 데이터의 동기화를 위한 수많은 Watcher가 생성되므로, 반대로 성능 저하가 발생할 수 있습니다.
이런 문제들 때문에, 리액트에서는 다른 프레임워크와는 다르게 단방향 데이터 바인딩을 사용합니다.
단방향 데이터 바인딩은 단 하나의 Watcher가 자바스크립트의 데이터 갱신을 감지하여 사용자의 UI 데이터를 갱신합니다. 사용자가 UI를 통해 자바스크립트의 데이터를 갱신할 때는, 이벤트를 통해 갱신하게 됩니다. 이처럼 단방향 데이터 바인딩은 하나의 Watcher를 사용하기 때문에 양방향 데이터 바인딩이 가지는 성능적인 이슈를 해결하고 더 확실하게 데이터를 추적할 수 있게 해줍니다.
JSX
리액트에서는 JSX라는 독특한 문법을 가지고 있습니다. JSX는 자바스크립트와 HTML을 동시에 사용하며, HTML에 자바스크립트의 변수들을 바로 사용할 수 있는 일종의 템플릿 언어(Template language)입니다.
const App = () => {
const hello = 'Hello world!';
return <div>{hello}</div>;
};
리액트는 위와 같이 자바스크립트에서 HTML 태그를 사용할 수 있으며, 자바스크립트 변수를 HTML 태그에서 바로 호출하여 사용할 수 있습니다. 자바스크립트 개발자들에게 이 JSX가 굉장히 이상한 문법이지만, 다른 언어들을 잠깐 살펴보면, 조금 더 쉽게 이해할 수 있습니다.
예를 들어 자바의 jsp에서는 다음과 같이 HTML 태그안에서 자바의 변수를 사용하는 것을 알 수 있습니다.
<div><%= hello %></div>
물론 리액트의 JSX 문법과는 조금 차이가 있지만, 이렇게 JSX를 자바스크립트의 문법이 아닌 일종의 템플릿 문법으로 기억하면 조금 더 쉽게 이해할 수 있습니다.
선언형 프로그래밍
프로그래밍에는 명령형 프로그래밍과 선언형 프로그래밍으로 구별할 수 있습니다. 명령형 프로그래밍은 프로그래밍을 할 때 어떻게(How)
에 집중하는 것을 말하며 선언형 프로그래밍은 무엇(What)
에 집중하여 프로그래밍을 하는 것을 말합니다.
예를 들어 택시를 타고 집으로 돌아가는 과정을 명령형 프로그래밍으로 설명하면 “첫 번째 사거리에서 우회전하고 삼거리가 나올 때까지 직진한 후, 삼거리에서 좌회전하면 우리 집입니다”와 같이 어떻게
집으로 돌아가는지에 집중하여 설명합니다. 하지만, 선언형 프로그래밍으로 이 과정을 설명하면 “우리 집은 XXX번지 입니다”와 같이 집이라는 어떤 것
에 집중하여 설명하게 됩니다.
그럼 실제 소스 코드를 보면서 이해해 봅시다.
- 명령형 프로그래밍
const double = (arr) => {
let results = [];
for (let i = 0; i < arr.length; i ++) {
results.push(arra[i] * 2);
}
return results;
}
- 선언형 프로그래밍
const double = (arr) => {
return arr.map((elem) => elem * 2);
}
위에 두 함수는 같은 동작을 하는 함수로써, 주어진 배열의 값을 두 배로 만드는 동작을 수행합니다. 첫번째 명령형 프로그래밍으로써 주어진 배열의 값을 두 배로 늘리기 위해, for 문을 사용했으며, i변수와 배열의 크기를 사용하여 배열의 값을 하나씩 가져와 두 배로 만든 후, result라는 새로운 배열에 값을 추가한 후, 결과값으로 반환합니다. 이렇게 명령형 프로그래밍은 과정을 중심으로 프로그래밍을 하게 됩니다.
두 번째 함수는 선언형 프로그래밍으로써 map 함수를 사용하여 주어진 배열값을 두 배로 만들어 반환합니다. map이 어떻게 동작하는지는 크게 신경쓰지 않고 결과인 배열값을 두 배로 만들기에 집중하여 프로그래밍을 합니다.
이처럼 라이브러리나 프레임워크 등을 사용하여 비선언형적인 부분을 캡슐화함으로써 명령형 프로그래밍 언어로 선언형 프로그래밍을 할 수 있습니다. 리액트에서는 특히 JSX를 사용하기 때문에 선언형 프로그래밍을 더욱 활용하고 있음을 알 수 있습니다.
- 명령형 프로그래밍
<ul id=”list”></ul>
<script>
var arr = [1, 2, 3, 4, 5]
var elem = document.querySelector("#list");
for(var i = 0; i < arr.length; i ++) {
var child = document.createElement("li");
child.innerHTML = arr[i];
elem.appendChild(child);
}
</script>
위의 예제는 자바스크립트를 사용하는 HTML에 새로운 리스트 아이템을 추가하는 코드입니다. 위의 코드는 명령형 프로그래밍으로써 새로운 리스트를 표시할 ul 태그를 생성하고 자바스크립트의 querySelector를 사용하여 표시할 위치를 가져온 후, for 문을 사용하여 하나씩 리스트에 아이템을 추가하고 있습니다.
- JSX를 사용한 선언형 프로그래밍
const arr = [1, 2, 3, 4, 5];
return (
<ul>
{arr.map((elem) => (
<li>{elem}</li>
))}
</ul>
);
리액트의 JSX 문법을 사용하면 위와 같이 HTML안에서 map 함수를 사용하여 리스트 아이템을 추가할 수 있습니다. 이렇게 JSX를 사용하면 자바스크립트뿐만 아니라 HTML에서도 선언형 프로그래밍을 할 수 있습니다.
컴포넌트 기반
리액트로 웹 서비스를 개발할 때, 컴포넌트
라고 부르는 작고 고립된 코드를 사용하여 UI를 구성하게 됩니다.
const Title = () => {
return <h1>Hello world</h1>;
};
const Button = () => {
return <button>This is a Button</button>;
};
const App = () => {
return (
<div>
<Title />
<Button />
</div>
);
};
이렇게 작고 고립된 컴포넌트는 재사용을 할 수 있으며 이런 재사용을 통해 개발 생산성을 향상시킬 수 있습니다. 또한 이렇게 작고 고립된 컴포넌트는 테스트하기 쉬워 코드를 유지보수하는데도 크게 도움이 됩니다.
완료
이번 블로그 포스트에서는 리액트란 무엇인지, 리액트의 장점은 무엇인지 살펴보았습니다. 앞으로 이어질 블로그에서는 이런 리액트를 사용하는 방법에 대해서 알아보겠습니다.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku
가 개발한 앱을 한번 사용해보세요.Deku
가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.