25.08
사이드 프로젝트에 주로 Next.js를 사용했습니다. 하지만 쓰면 쓸수록 이게 프론트엔드 공부가 되긴 하는건지, Next.js 디버깅 능력만 느는건 아닌지 헷갈렸습니다. 문득 기술 스택으로 리액트나 Next.js 이외의 선택지를 고려해본 적이 없었음을 깨달았습니다.
2020년대에 프론트엔드 개발 공부를 시작했다면 리액트를 중점적으로 배웠을 가능성이 높습니다. 아마 회사에서 가장 많이 사용되는(=취업에 도움이 되는) 프론트엔드 도구가 리액트이기 때문일 것입니다.
하지만 리액트가 유일한 도구는 아닙니다. 리액트가 처음 출시되기 이전에도 프론트엔드 개발은 존재했습니다. 10년이 넘게 지난 요즈음 상황을 보면 state-of-js의 설문조사에서 보이듯이 Next.js/리액트에 화가 난 개발자들이 많습니다. astro같은 몇몇 대안들이 주목받고 있기도 합니다.
왜 리액트여야하는지 돌아보기 좋은 시점입니다. 왜 DOM을 직접 조작하지 않고 리액트를 쓰는 걸까요? 굳이 리액트를 사용하지 않아도 되는 상황이 있지는 않을까요?
리액트 이전의 프론트엔드 개발은 DOM API를 사용해 직접 UI를 수정하는 방식이었습니다. 버튼이 눌릴 때 숫자를 늘리고 싶다면 아래처럼 직접 DOM 요소에 접근해 내용물을 변경합니다.
const buttonElement = document.getElementById(
"some-button-id",
);
const textElement = document.getElementById("some-text-id");
let count = 0;
function increment() {
count++;
textElement.textContent = count.toString();
}
buttonElement.addEventListener("click", increment);
좀 더 복잡한 예제를 살펴봅시다. 쇼핑몰을 예제로 DOM API 방식이 머리아파지는 부분을 살펴보겠습니다.
처음에는 단순하게 장바구니 버튼을 클릭하면 상품이 추가되는 기능만 있습니다.
상품 A₩5,000
이후 기획이 변경되어 상품을 여러개 추가할 수 있게 되었다고 합시다. 이미 장바구니에 있는 상품을 다시 클릭하면 수량이 증가하고, 수량이 0이 되면 항목이 삭제되어야합니다. 이제 개발자는 기존에 만들어둔 장바구니 UI를 어떻게 업데이트할지 고민해야 합니다.
장바구니가 비어있습니다.
네트워크 요청이 필요하다면 더 복잡한 상황도 발생합니다. 상품 목록 페이지에서 사용자가 필터를 변경할 때마다 서버에서 새로운 데이터를 가져와야 하는 기능을 개발하고 있다고 가정해봅시다.
아래 필터를 차례대로 빠르게 클릭하여 race condition을 재현해봅시다.
최종적으로 색상을 선택했지만 실제 필터는 가격으로 되어있는 것이 보이나요?
이러한 버그를 막기 위해서 개발자는 어떻게 마지막 요청만 남길지 고민해야 합니다. DOM API로 이를 구현하려면 각 요청의 상태를 추적하고, 응답이 도착했을 때 그것이 최신 요청인지 확인해야 합니다. 이는 번거럽고 실수하기 쉬운 작업입니다.
DOM API 사용 시 가장 큰 어려움 중 하나는 상태가 변경될 때 기존 DOM 요소들을 어떻게 처리할지 결정하는 것입니다. 개발자는 기존 요소를 완전히 제거하고 새로 만들 것인지, 기존 요소를 재사용하고 내용만 업데이트할 것인지, 어떤 이벤트 리스너들을 제거하고 다시 추가해야 하는지, 애니메이션이나 트랜지션은 어떻게 처리할 것인지 등의 복잡한 결정을 내려야 합니다.
리액트는 상태로부터 UI를 그리는 작업을 대신해줍니다.
개발자는 상태가 변경될 때마다 어떤 UI가 보여져야 하는지 선언만 하고 DOM API에 어떻게 명령해 그 UI를 만들지는 신경쓰지 않아도 됩니다. 이는 리액트 문서에서도 언급됩니다.
React는 상호작용이 많은 UI를 만들 때 생기는 어려움을 줄여줍니다. 애플리케이션의 각 상태에 대한 간단한 뷰만 설계하세요. 그럼 React는 데이터가 변경됨에 따라 적절한 컴포넌트만 효율적으로 갱신하고 렌더링합니다.
아래 컴포넌트를 DOM API로 구현한 것과 리액트로 구현한 것을 비교해봅시다.
0
DOM API를 사용하면 어떻게(HOW) 변경할지 단계별로 명령합니다.
<!-- html -->
<div>
<div id="counter">0</div>
<button id="counter-button">+1</button>
</div>
// js
let count = 0;
const text = document.getElementById("counter");
const button = document.getElementById("counter-button");
function increment() {
count++;
text.textContent = count;
}
button.addEventListener("click", increment);
리액트를 사용하면 상태에 따라 UI가 무엇(WHAT)이어야 하는지만 선언합니다.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</div>
);
}
상태 변화가 복잡하고 빈번한 애플리케이션에서는 리액트의 장점이 명확하게 드러납니다. 예를 들어 위에서 예제로 보였던 쇼핑몰처럼 사용자 로그인과 로그아웃, 장바구니 관리, 상품 필터링과 정렬 등 여러 상태가 서로 영향을 주는 경우나 사용자 인터랙션이 많아서 DOM이 자주 업데이트되는 경우에는 리액트가 개발 복잡도를 크게 줄여줍니다.
하지만 회사 소개 페이지나 블로그 글처럼 상태 변화가 거의 없는 정적인 페이지나, 단순한 폼 제출 정도의 기능만 있는 웹사이트에서는 리액트가 과할 수 있습니다. 이런 경우 오히려 라이브러리 용량과 초기 로딩 시간만 늘어날 뿐입니다.
리액트가 해결해주는 불편함이 무엇인지 돌아보았습니다. 기술의 발전은 계속될 것이고, 새로운 패러다임도 등장하겠지만 중요한 것은 각 도구가 해결하려는 문제를 이해하고, 상황에 맞는 선택을 하는 것입니다.