요즘의 프론트엔드 개발자라면 누구나 알고 있어야 하는 리액트. 딱히 프론트엔드 개발자가 아니더라도 웹개발에 관심이 있는 개발자라면 누구나 리액트가 인기있고 대세라는 것은 모두 알고 있는 사실이다. 개발자에게 의미있는 NPM 다운로드 수 기준으로만 본다면, 모던 웹 라이브러리/프레임워크(Modern Web Library/Framework) 즉, Angular, React, Vue 이 3대장 사이에서도 압도적인 다운로드 숫자를 가지며, 프로덕션 환경에서 가장 많이 쓰인다는 jQuery도 따라올 수 없는 수준이다. 그리고 이 격차는 점점 더 벌어지고 있다.
React의 출시는 2013년이지만 본격적으로 인기가 많아진 시기는 React Hooks가 출시된 2019년 즈음부터인것 같다. 그 이전에는 클래스형 컴포넌트가 주를 이루었고, 함수형 컴포넌트가 없었던 것은 아니지만 함수형 컴포넌트는 단순한 UI 렌더링으로 사용되었다. 그러나 Hooks가 업데이트된 버전부터는 함수형 컴포넌트에서도 상태관리와 라이프사이클 기능을 간단하게 사용할 수 있게 되어, 함수형 컴포넌트의 사용 범위가 크게 확장되었다.
그렇다면 리액트는 왜 이렇게 인기가 많은 것일까? 압도적인 커뮤니티 수, 페이스북(Meta)의 지원, 그리고 Next.js, Gastby 등 리액트 기반의 다른 라이브러리로 이어질 수 있는 확장성 등의 굉장한 장점도 있지만, 프론트엔드 개발자 관점에서 좀 더 들여다 본다면 컴포넌트 기반의 패턴, JSX, Virtual DOM 이라는 3가지 주제로 나눌 수 있을 것 같다.
컴포넌트 기반의 패턴
컴포넌트 패턴은 리액트의 핵심 개념 중 하나로, 애플리케이션을 독립적이고 재사용 가능한 작은 부품으로 나누어 관리하는 방법을 의미한다.
좀 더 자세하게 설명하자면, 컴포넌트 패턴은 UI를 구성하는 각 부분을 별도의 컴포넌트로 정의하는 방법이다. 각 컴포넌트는 자체적으로 독립적이며, 필요한 경우 다른 컴포넌트를 포함하거나 데이터와 상호작용할 수 있다. 가장 쉬운 예로서, 하나의 디자인으로 된 버튼 UI를 만들어두고 필요한 곳마다 재사용 하는 것이다.
// 컴포넌트 구현
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
// 다른 곳에서 재사용
function App() {
return (
<div>
<Button label="Submit" onClick={() => console.log('Submitted!')} />
<Button label="Cancel" onClick={() => console.log('Cancelled!')} />
</div>
);
}
그렇다면 리액트에서 컴포넌트 패턴을 사용함으로써 얻는 이점은 무엇일까?
1. 재사용성(Reusability)
컴포넌트는 독립적이고 재사용이 가능하도록 설계된다. 위 예시와 같이 동일한 기능을 여러번 구현할 필요 없이, 한번 작성한 컴포넌트를 여러 곳에서 사용할 수 있다. 재사용 가능한 컴포넌트는 코드 중복을 줄이고 유지보수를 쉽게 한다.
2. 모듈성(Modularity)
모듈성은 애플리케이션을 작은 단위로 나누어 각각 독립적으로 개발, 테스트 유지보수 할 수 있게 하는 것을 말한다. 아래의 예시에서, 'Header', 'MainContent', 'Footer' 이 3가지 각각의 컴포넌트는 독립적으로 개발되고 테스트될 수 있다. 만약, 'Header'의 디자인이나 기능을 변경해야 한다면, 'Header' 컴포넌트만 수정하면 된다.
// 컴포넌트 구현
function Header() {
return <header>Header Section</header>;
}
function MainContent(data = 'Main Content Section') {
return <main>{data}</main>;
}
function Footer() {
return <footer>Footer Section</footer>;
}
// 만들어놓은 'Header', 'MainContent', 'Footer' 를 사용하는 부분
function App() {
return (
<div>
<Header />
<MainContent />
<Footer />
</div>
);
}
3. 상태 관리의 용이성(Ease of State Management)
리액트 컴포넌트는 각각의 상태를 관리할 수 있다. 함수형 컴포넌트는 보통 'useState' 훅을 사용하여 상태를 관리하고, 이를 통해 컴포넌트 간의 상태 관리가 독립적으로 이루어질 수 있도록 도와준다.
// 컴포넌트 구현
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// 컴포넌트 사용
function App() {
return (
<div>
<Counter />
</div>
);
}
여기서 'Counter' 컴포넌트는 자체적으로 상태('count') 를 관리하고, 버튼 클릭 시 상태를 업데이트 한다. 이는 다른 컴포넌트와의 상태 충돌 없이 독립적으로 동작할 수 있게 한다.
+)추가적으로, 보통 재사용성을 말할 때, UI 를 중심으로 예를 들지만, UI 뿐 아니라 로직만을 따로 모아 재사용할 수도 있고 이를 커스텀 훅(custom hooks)이라 한다. 커스텀 훅을 통해 특정 로직을 캡슐화하고 여러 컴포넌트에서 재사용할 수 있도록 한다. 위 3가지 장점을 모두 활용한 예를 보도록 하자. API에서 데이터를 가져오는 로직을 여러 컴포넌트에서 사용한다고 가정했을 때, 이를 커스텀 훅으로 추출할 수 있다.
// 커스텀 훅 정의
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
function async fetchData() {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetch;
// 커스텀 훅 사용
import React from 'react';
import useFetch from './useFetch';
function App() {
const { data, loading, error } = useFetch('https://api.example.com/data');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<MainContent content={data} /> {/* 위에서 작성한 'MainContent' 컴포넌트 활용 */}
</div>
);
}
export default App;
이렇게 하면 데이터를 가져오는 로직이 'useFetch' 훅으로 추출되어, 다른 컴포넌트에서도 동일한 로직을 반복하지 않고 재사용할 수 있다. 이러한 사용을 통해, 하나의 비즈니스 로직, 또는 도메인 로직을 여러 컴포넌트에서 활용할 수 있고, 하나의 UI를 여러 컴포넌트, 또는 페이지에서 재활용 할 수 있다. 즉, 비즈니스와 UI 를 분리함으로서 이들 간의 관심사 분리 또한 가능하다.
JSX를 통한 단일 파일 컴포넌트 (HTML + CSS + JS)
JSX(JavaScript XML)
JSX는 리액트에서 UI를 정의할 때 사용되는 문법 확장으로, HTML과 유사한 문법을 자바스크립트 코드 안에서 사용할 수 있게 해준다.
그렇다면 리액트에서 JSX를 사용함으로써 얻는 이점은 무엇일까?
1. 가독성 및 유지보수성
JSX는 HTML과 유사한 문법을 사용하기 때문에, UI 구조를 직관적으로 이해하고 유지보수할 수 있다. 이는 컴포넌트의 구조와 내용을 쉽게 파익할 수 있어 코드의 가독성이 높아진다.
function App() {
return (
<div className="app">
<header className="app-header">
<h1>Welcome to My App</h1>
</header>
<main>
<p>This is a simple example of JSX usage.</p>
</main>
</div>
);
}
2. JavaScript와의 강력한 통합
JSX는 자바스크립트와 자연스럽게 통합되므로, UI렌더링 로직과 자바스크립트 코드를 함께 작성할 수 있다. 이를 통해 복잡한 UI 동작을 쉽게 구현할 수 있다.
function App() {
const items = ['Item 1', 'Item 2', 'Item 3'];
return (
<ul>
{items.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
}
위 예제에선 'items' 배열의 각 요소를 반복하여 '<li>' 태그로 렌더링하는 로직을 자바스크립트 코드와 함께 작성할 수 있다.
3. 스타일링의 통합 관리
JSX를 사용하면 인라인 스타일링을 포함하여 다양한 스타일링 방법을 사용할 수 있다. 스타일링 로직을 컴포넌트 내에 포함시킴으로써 스타일과 UI 로직을 함께 관리할 수 있다. (물론 스타일링을 따로 정의하는 것도 가능하다.)
const headerStyle = {
backgroundColor: 'blue',
color: 'white',
padding: '10px'
};
function App() {
return (
<div>
<header style={headerStyle}>
<h1>Styled Header</h1>
</header>
</div>
);
}
즉, JSX를 사용하면 UI, Styling, 그리고 JS 로직을 하나의 파일에서 관리할 수 있다. 이는 단일 책임 원칙(Single Responsibility Principle)을 적용하여 모듈화를 쉽게 하고, 파일 하나만으로 특정 컴포넌트의 모든 코드를 관리할 수 있어 편리하다. 이는 HTML, CSS, JS 까지 총 3가지의 파일을 번갈아가며 코딩하던 옛날 옛적과 비교했을 때, 엄청난 개발 효율의 향상을 가져왔다. 즉, 개발 속도가 향상되었다.
리액트로만 작성된 아래의 컴포넌트를 HTML, CSS, JS 세 가지 종류로 다시 분리해보자.
import React from 'react';
function App() {
return (
<div style={app}>
<header style={app-header} onClick={() => alert('Header Clicked!')}>
<h1>Welcome to My App</h1>
</header>
<main style={main}>
<p>This is a simple example of JSX usage.</p>
</main>
</div>
);
}
export default App;
const app = {
text-align: center;
font-family: Arial, sans-serif;
}
const appHeader = {
background-color: blue;
color: white;
padding: 10px;
}
const main = {
padding: 20px;
}
아래는 HTML, CSS, JS 순으로 분리했을 때이다.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app">
<header class="app-header">
<h1>Welcome to My App</h1>
</header>
<main>
<p>This is a simple example of JSX usage.</p>
</main>
</div>
<script src="script.js"></script>
</body>
</html>
/* styles.css */
.app {
text-align: center;
font-family: Arial, sans-serif;
}
.app-header {
background-color: blue;
color: white;
padding: 10px;
}
main {
padding: 20px;
}
// script.js
document.addEventListener('DOMContentLoaded', () => {
console.log('App is loaded');
// Example of dynamic behavior
const header = document.querySelector('.app-header');
header.addEventListener('click', () => {
alert('Header clicked!');
});
});
양이 꽤 많아졌다. 이 세 개의 파일들을 번갈아가며 코딩을 한다고 생각해보자. 스타일링은 차치하고라도, JS 에서 나타난 'header' 라는 요소를 찾기 위해 HTML에선 위에서 아래로 코드를 훑어내려와야 한다(반대의 경우에도). 그러나 React 에서는 header 요소를 찾는다면, 그 자리에서 바로 JS 코드를 주입하는 것이 가능하다. 아주 쉬운 예제라서 별로 그리 어렵지 않게 느껴질 수 있겠지만, 현업에서는 각각의 파일이 몇 백 줄이 될 수 있다는 것을 생각할 때, 이는 굉장히 큰 장점이 될 수 있다.
그렇다면, 모던 웹 프레임워크 3대장과 비교했을 때도 위와 같은 결과가 나올지, Chat GPT를 사용해서 만들어달라고 부탁해보았다. (3가지가 각각 프레임워크, 또는 라이브러리로 나눠질 수 있고, 해결하려고 하는 문제가 다르다는 것을 차치하고) React 만 사용해본 나로선, 다른 2개의 프레임워크가 최적화되어 작성되었는지 정확하게 확인할 수는 없지만, React가 가장 효율적으로 보인다.
React, Vue, Angular 세가지 모던 웹 프레임워크 비교
- React
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
return <h1>Hello, World!</h1>;
}
ReactDOM.render(<App />, document.getElementById('root'));
- Vue
<!DOCTYPE html>
<html>
<head>
<title>Hello Vue</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
</head>
<body>
<div id="app">
{{ message }}
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'Hello, World!'
}
});
</script>
</body>
</html>
- Angular
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: '<h1>{{ title }}</h1>',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Hello, World!';
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>HelloAngular</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<app-root></app-root>
</body>
</html>
Virtual DOM
Virtual DOM은 실제 DOM(Document Object Model)의 가상 표현이다. 리액트는 UI의 상태가 변경될 때마다 전체 UI 를 다시 렌더링하지 않고, 변경된 부분만 DOM에 업데이트하기 위해 Virtual DOM을 사용한다. Virtual DOM은 메모리 내에 존재하는 가벼운 JavaScript 객체로, 실제 DOM 과 유사한 구조를 가지고 있다.
그렇다면 그렇다면 왜 Virtual DOM을 사용할까? 그냥 DOM을 변경하는 것은 그렇게 느리고 효율적이지 않은 것인가?
그리고 자바스크립트 엔진은 성능이 더 좋아지고 있는데, 정확히 어떤 부분 때문에 DOM이 느려지는 것인가?
정확히 말하자면, 직접적인 DOM 조작이 전체적인 동작을 느려지게 하는 것이 아니라, 그 이후에 일어나는 일 때문에 작업이 느려지는 것이다. 'DOM Tree → Render Tree → Layout(Reflow) → Paint'라고 요약될 수 있는 브라우저의 작업흐름을 본다면, DOM 변경이 일어날 때마다 이러한 과정이 필요 이상으로, 매번 반복되는 것을 알 수 있을 것이다. 예를 들어보자. 열 개의 아이템이 있는 리스트가 있다. 유저는 첫번째 아이템을 체크한다. 대부분의 자바스크립트 프레임워크는 전체 리스트를 다시 생성한다. 하나의 변경만 필요한 곳에서 열 배의 계산이 일어난다.
복잡한 SPA, 즉 React 안에서는 DOM 조작이 많이 발생한다. 그 말은 변화를 적용하기 위해 브라우저에서 많은 연산이 필요하다는 뜻이고, 전체적인 프로세스를 비효율적으로 만든다. 바로 이러한 부분을 해결하기 위해 Virtual DOM이 등장하는 것이다. DOM 조작이 필요한 View(화면)에 변화가 있을 때, 먼저 Virtual DOM에 적용한다. 이 Virtual DOM은 렌더링도 되지 않기 때문에 연산 비용이 적다. 이 Virtual DOM은 단지 자바스크립트로 작성된 Object 들일뿐이다. 그리고 최종적인 변화는 실제 DOM에 던져준다. 딱 한번만. Layout 계산과 리렌더링의 규모는 커지지만, 이렇게 한번의 계산으로 묶어서 적용시킴으로써 연산의 횟수를 줄이는 것이다.
+) Declarative의 등장
사실, 이 과정은 Virtual DOM 없이도 이뤄질 수 있다. 변화가 있으면 DOM fragment에 적용한 다음 기존 DOM에 던져주면 된다. 그러면 Virtual DOM 이 해결하려는 것이 무엇이냐? 그 DOM fragment를 관리하는 과정을 수동으로 하나하나 작업할 필요 없이, 자동화하고 추상화 하는 것이다. 그뿐 아니라, 이 작업을 직접 수행한다면, 기존 값 중 어떤게 바뀌었고, 어떤게 바뀌지 않았는지 계속 파악하고 있어야하는데, 이것도 Virtual DOM이 자동으로 해주는 것이다. 어떤게 바뀌었고, 바뀌지 않았는지 알아내준다.
그리고 DOM관리를 Virtual DOM이 하도록 함으로써, 컴포넌트가 DOM 조작 요청을 할 때 다른 컴포넌트 들과 상호작용을 하지 않아도 되고, 특정 DOM을 조작할 것이라든지, 이미 조작했다던지에 대한 정보를 공유할 필요가 없다. 즉, 각 변화들의 동기화 작업을 거치지 않으면서도 모든 작업을 하나로 묶어줄 수 있다는 것이다.
그렇다면 Virtual DOM 의 동작방식을 자세하게 들여다보자.
1. 초기 렌더링 (Initial Rendering)
- 리액트 애플리케이션이 처음 로드되면, 컴포넌트 구조를 기반으로 메모리에 저장되는 경량화된 구조인 Virtual DOM 트리가 생성된다.
- 리액트는 이 가상 DOM 을 기반으로 실제 DOM을 생성하고, 브라우저에 렌더링한다.
2. 상태 변화 (State Change)
- 애플리케이션의 상태(state)나 props 가 변경되면, 리액트는 새로운 Virtual DOM을 생성한다.
- 이 새로운 가상 DOM은 변경된 상태를 반영한 이전 가상 DOM과는 다른 구조를 가진다.
3. Diffing (차이 비교)
- 새로운 가상 DOM과 이전 가상 DOM을 Diffing 알고리즘을 통해 비교하여 변화가 일어난 부분을 찾는다.
- 이 diffing 알고리즘은 O(n)의 복잡도를 가진다. 여기서 n은 element의 갯수이다.
한 트리를 다른 트리로 변환하기 위한 최소한의 연산 수를 구하는 문제를 풀기 위한 최첨단의 일반적인 알고리즘은 n개의 엘리먼트가 있는 트리에 대해 O(N^3)의 복잡도를 가진다. 즉 한 페이지에 1000의 엘리먼트가 있다면 10억 번의 비교 연산이 필요하다는 것이다.
그러나 리액트는 다음의 가정을 통해 이를 O(n) 으로 줄인다:
1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
예를 들어, <a> → <img> 로 바뀌면, 변경된 트리 전체를 재구축한다.
2. 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.
즉, key라는 속성을 통해 변경된 엘리먼트와 변경되지 않은 엘리먼트를 구분한다.
4. Patching
- 리액트는 차이 비교 결과를 실제 DOM에 반영한다.
- 이 과정은 실제 DOM을 최소한으로 조작하여 성능을 최적화한다. 즉, 변경된 부분만 업데이트한다는 것이다.
5. Rendering
- 변경 사항이 실제 DOM에 반영되면, 브라우저는 이를 렌더링하여 사용자에게 보여준다.
- 이때, 리액트는 최소한의 DOM조작을 통해 성능을 최적화하므로, 전체 DOM을 다시 그리는 대신 변경된 부분만 다시 그린다.
+) React 18 부터는 여러 상태 업데이트를 하나의 렌더링 주기로 묶어 Batch 방식으로 처리한다.
+) Diffing 부터 Rendering 까지의 과정을 Reconciliation(재조정) 이라고도 한다.
위의 크게 3가지 사항이, 내가 느끼는, 요즘 현업에서 리액트를 가장 많이 쓰는 3가지 이유인 것 같다. 컴포넌트 기반의 패턴을 통한 유지보수성과 개발 효율성 향상, JSX를 통한 개발 효율성 향상, 그리고 Virtual DOM 통한 Performance 향상 및 개발 효율성 향상. 결국 공통적인 건 개발 효율성 향상. 개발 효율성 향상이라는 말이 너무 광범위하고 모든 것을 포함하는 말이긴하지만, 스타트업 같이 빠르게 개발하고, 빠르게 테스트하고, 빠르게 변해야하는 환경에서는 리액트는 굉장히 훌륭한 개발 도구이다.
반면에 이미 사업이 궤도에 올라선 (리액트를 사용하지 않는) 대기업 같은 곳에서는 그래서 굳이 리액트를 도입할 필요가 없다. 도입비용과 프로덕션의 안정적인 운영을 고려했을 때, 굳이 최신 기술을 사용하겠다고 모험을 감수할 필요가 없다. 그래서 개발 도구와 프로덕트의 구조. 비즈니스의 상황과 구조. 이 둘은 따로 떨어져 생각할 수 없는 것이다.
References
NPM Trends - https://npmtrends.com/angular-vs-jquery-vs-react-vs-vue
The one thing that no one properly explains about React- Why Virtual DOM, S Kokanduri, 2016. - https://saiki.hashnode.dev/the-one-thing-that-no-one-properly-explains-about-react-why-virtual-dom
The Power of React Virtual DOM: Revolutionizing DOM Manipulation, R Purohit, 2023. - https://www.dhiwise.com/post/the-power-of-react-virtual-dom-revolutionizing-dom-manipulation
[번역] 리액트에 대해서 그 누구도 제대로 설명하기 어려운 것 – 왜 Virtual DOM 인가? - https://velopert.com/3236
React: The Virtual DOM - https://www.codecademy.com/article/react-virtual-dom
https://legacy.reactjs.org/docs/reconciliation.html
Reconciliation - https://legacy.reactjs.org/docs/reconciliation.html
10 Key Reasons Why You Should Use React for Web Development, O Jutsulyak, 2023. -https://www.techmagic.co/blog/why-we-use-react-js-in-the-development/
How React ACTUALLY works (DEEP DIVE 2023) - https://www.youtube.com/watch?v=za2FZ8QCE18&ab_channel=FrontStart
'React' 카테고리의 다른 글
리액트 상태 관리 전략 - Context API 와 상태관리 라이브러리 (0) | 2024.08.09 |
---|---|
React - useQuery 를 사용할 때 에러가 발생한다면..? (0) | 2024.07.11 |
Suspense in React | 리액트 서스펜스 (0) | 2024.06.22 |
ErrorBoundary in React || 리액트 에러 바운더리 (0) | 2024.06.17 |