Seongyeol Yi

리액트 이외의 선택지

This post is not yet available in English and is shown in Korean.

사이드 프로젝트에 주로 Next.js를 사용했었다. 하지만 쓰면 쓸수록 이게 프론트엔드 공부가 되긴 하는건지, Next.js 디버깅 능력만 느는건 아닌지 헷갈렸다. 문득 기술 스택으로 리액트나 Next.js 이외의 선택지를 고려해본 적이 없었음을 깨달았다.

2020년대에 프론트엔드 개발 공부를 시작했다면 리액트를 중점적으로 배웠을 가능성이 높다. 아마 회사에서 가장 많이 사용되는(=취업에 도움이 되는) 프론트엔드 도구가 리액트이기 때문일 것이다.

하지만 리액트가 유일한 도구는 아니다. 리액트가 처음 출시되기 이전에도 프론트엔드 개발은 존재했다. 10년이 넘게 지난 요즈음 상황을 보면 state-of-js의 설문조사에서 보이듯이 Next.js/리액트에 화가 난 개발자들이 많다. astro같은 몇몇 대안들이 주목받고 있기도 하다.

공부하고 취준할 때는 당연히 받아들여 썼지만 왜 리액트를 쓰는지 돌아보기 좋은 시점이다. 왜 DOM을 직접 조작하지 않고 리액트를 쓰는 걸까? 굳이 리액트를 사용하지 않아도 되는 상황이 있지는 않을까?

DOM API 복습

리액트 이전의 프론트엔드 개발은 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로 쇼핑몰 만들기

위 코드만 봤을 때는 DOM을 직접 만지는 것도 괜찮아보인다. 이번에는 좀 더 복잡한 예제를 살펴보자. 쇼핑몰을 예제로 DOM API 방식이 머리아파지는 부분을 살펴보겠다.

처음에는 단순하게 장바구니 버튼을 클릭하면 상품이 추가되는 기능만 있다.

상품 목록

상품 A

장바구니

이후 기획이 변경되어 “이미 장바구니에 있는 상품을 다시 클릭하면 수량이 증가하고, 수량이 0이 되면 항목이 삭제되는” 기능이 추가되었다고 가정해보자. 이제 개발자는 기존에 만들어둔 장바구니 UI를 어떻게 업데이트할지 고민해야 한다.

  • 새로운 상품 항목을 추가할 때는 새 DOM 요소를 생성해야 하지만…
  • 기존 상품의 수량만 변경할 때는 기존 요소를 찾아서 텍스트만 바꿔야 하고…
  • 상품을 삭제할 때는 해당 DOM 요소를 완전히 제거하며 요소에 연결된 이벤트 리스너들도 모두 정리해야 한다.

상품 목록

포토카드
키링
스티커
머그컵

장바구니

  • 장바구니가 비어있습니다.

더 복잡한 상황도 발생한다. 상품 목록 페이지에서 사용자가 필터를 변경할 때마다 서버에서 새로운 데이터를 가져와야 하는 기능을 개발하고 있다고 가정해보자. 아래 필터를 차례대로 빠르게 클릭하여 race condition을 재현해보자.

  1. ‘가격: 10,000원 이하’ (1.5초 소요) 클릭
  2. ‘색상: 블랙’ (0.5초 소요) 클릭

상품 필터

상품 목록

  • 포토카드 - ₩5000 (블랙)
  • 키링 - ₩15000 (블랙)
  • 스티커 - ₩4000 (화이트)
  • 머그컵 - ₩12000 (화이트)

라디오 버튼은 색상에 가있는데 실제 필터는 가격으로 되어있는 것이 보이는가? 이때 개발자는 어떻게 마지막 요청만 남길지 고민해야 한다. DOM API로 이를 구현하려면 각 요청의 상태를 추적하고, 응답이 도착했을 때 그것이 최신 요청인지 확인해야 한다. 현재 어떤 요청이 진행 중인지, 어떤 DOM 요소가 어떤 요청과 연결되어 있는지를 일일이 추적하고 관리하는 것은 꽤나 머리아픈 작업이다.

DOM API 요약

DOM API 사용 시 가장 큰 어려움 중 하나는 상태가 변경될 때 기존 DOM 요소들을 어떻게 처리할지 결정하는 것이다. 개발자는 기존 요소를 완전히 제거하고 새로 만들 것인지, 기존 요소를 재사용하고 내용만 업데이트할 것인지, 어떤 이벤트 리스너들을 제거하고 다시 추가해야 하는지, 애니메이션이나 트랜지션은 어떻게 처리할 것인지 등의 복잡한 결정을 내려야 한다.

리액트 등장

리액트는 상태로부터 UI를 그리는 작업을 대신해준다.

UI = f(state)

개발자는 상태가 변경될 때마다 어떤 UI가 보여져야 하는지 선언만 하고 DOM API에 어떻게 명령해 그 UI를 만들지는 신경쓰지 않아도 된다. 이는 리액트 문서에서도 언급된다.

React components receive data and return what should appear on the screen.

React는 상호작용이 많은 UI를 만들 때 생기는 어려움을 줄여줍니다. 애플리케이션의 각 상태에 대한 간단한 뷰만 설계하세요. 그럼 React는 데이터가 변경됨에 따라 적절한 컴포넌트만 효율적으로 갱신하고 렌더링합니다.

DOM API vs 리액트

아래 컴포넌트를 DOM API로 구현한 것과 리액트로 구현한 것을 비교해보자.

0

DOM API를 사용하면 어떻게(HOW) 변경할지 단계별로 명령한다.

let count = 0;
const counter = document.getElementById("counter");

function increment() {
  count++;
  // DOM을 직접 찾아서 수정
  counter.textContent = count;
  // 조건에 따라 스타일도 직접 변경
  if (count > 0) {
    counter.className = "positive";
  } else if (count < 0) {
    counter.className = "negative";
  } else {
    counter.className = "zero";
  }
}

const counterButton = document.getElementById("counter-button");
counterButton.addEventListener("click", increment);

리액트를 사용하면 상태에 따라 UI가 무엇(WHAT)이어야 하는지만 선언한다.

function Counter() {
  const [count, setCount] = useState(0);
  const className = (() => {
    if (count > 0) return "positive";
    if (count < 0) return "negative";
    return "zero";
  })();

  return (
    <div>
      <div className={className}>{count}</div>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

언제 리액트를 써야할까

상태 변화가 복잡하고 빈번한 애플리케이션에서는 리액트의 장점이 명확하게 드러난다. 예를 들어 위에서 예제로 보였던 쇼핑몰처럼 사용자 로그인과 로그아웃, 장바구니 관리, 상품 필터링과 정렬 등 여러 상태가 서로 영향을 주는 경우나 사용자 인터랙션이 많아서 DOM이 자주 업데이트되는 경우에는 리액트가 개발 복잡도를 크게 줄여준다.

하지만 회사 소개 페이지나 블로그 글처럼 상태 변화가 거의 없는 정적인 페이지나, 단순한 폼 제출 정도의 기능만 있는 웹사이트에서는 리액트가 과할 수 있다. 이런 경우 오히려 라이브러리 용량과 초기 로딩 시간만 늘어날 뿐이다.

마무리

리액트가 해결해주는 불편함이 무엇인지 돌아보았다. 기술의 발전은 계속될 것이고, 새로운 패러다임도 등장하겠지만 중요한 것은 각 도구가 해결하려는 문제를 이해하고, 상황에 맞는 선택을 하는 것이다.

프론트엔드 주제의 다른 글들

글 전체보기