비효율의 시작, 리액트

제이밍·2024년 11월 15일
2

해당 문서는 원문을 번역한 글입니다.

==

웹 개발에 그동안 많은 문제가 있었다는 것은 이미 일부 개발자들도 인지하고 있으며, 더 이상 문제가 되지도 않습니다.

모든 웹의 불필요한 요소들을 단번에 고칠 수 있는 궁극적인 해결책(!?)이라는 명분으로 더 많은 도구가 만들어지고 있지만 그것들이 과연 무언가를 성취하고 있을까요??

2016년 Hacker Noon 이라는 기사에 따르면

몇 년간 웹 개발을 떠났던 개발자와 여전히 현업에 있는 개발자 간의 대화를 다룬 이 글은, 8년이 지난 지금도 여전히 공감을 자아냅니다.

저는 백엔드로 돌아갈 겁니다. 저는 이렇게 많은 변경 사항과 버전과 에디션과 컴파일러와 트랜스파일러를 감당할 수 없습니다. JavaScript 커뮤니티가 이걸 따라갈 수 있다고 생각한다면 미친 짓입니다.

다만 결말에서 작가가 웹이 얼마나 멀리 그 영향력을 뻗칠지 과소평가한 점은 아쉬운 부분입니다.

리액트의 등장

페이스북 소셜 미디어가 대중에게 공개된 후, 빠르게 인기를 얻으며 다양한 기능이 추가되었고, 점점 복잡하고 느린 애플리케이션으로 변해갔습니다.

이를 해결하기 위해, 현재 Meta로 알려진 회사는 새로운 자바스크립트 라이브러리인 React를 개발하여 2013년에 공개했습니다. 오늘날 많은 프론트엔드 프레임워크가 존재하지만, React 생태계는 시장을 지배하며 다양한 독특한 특성을 강하게 드러내고 있습니다

페이스북이 React를 만들어 사용한 것보다 더 안타까운 것은, React가 출시된 후 전 세계의 많은 기술 회사들이 페이스북 만큼 성공하기 위해서는 자신들도 React가 필요하다고 믿기 시작한 것입니다.

페이스북의 명성이 점차 사라지고 있는 지금, 이 상황은 정말 아이러니한 상황입니다.

React가 도입한 가장 큰 변화 중 하나는, 업데이트 시 UI가 멈추지 않도록 모든 것을 비동기적으로 처리해야 한다는 점입니다.

따라서 어떤 상태를 변경하고 나중에 이를 확인하려고 할 때, 더 이상 단순히 이렇게 표현 할 수는 없습니다

let variable = "test"

if (variable === "test")
  console.log("hello")

React에서 변수의 상태를 변경하려면 비동기 메서드를 호출해야 합니다.

그 결과, if 문이 변수 값을 확인할 때, 해당 값이 이미 업데이트되었을 것이라는 보장이 없습니다.
이를 해결하기 위해 useEffect() 콜백을 사용하고 if 문을 그 안으로 이동시키는 방법이 있습니다. 예시는 다음과 같습니다

import { useState, useEffect } from "react"

const Example = () => {
  const [variable, setVariable] = useState("")

  useEffect(() => {
    setVariable("test");
  }, []);

  useEffect(() => {
    if (variable === "test")
      console.log("hello")
  }, [variable])
}

setVariable()은 절대 프로미스를 반환하지 않으므로, 상태 업데이트 후에도 콜백의 완료를 기다릴 수 없습니다.

따라서 useEffect()라는 콜백을 반드시 사용해야 하며, 이 콜백에는 다루려는 상태 변수를 배열 형태로 포함해야 합니다. 문제는 코드가 세 배로 길어지고 문법이 더 복잡해지는 것에 그치지 않습니다.

실제 문제는 콜백 지옥으로 인해 코드의 흐름이 지나치게 꼬이면서 디버깅이 사실상 불가능해진다는 것입니다. 이로 인해 개발자들이 console.log("passed here") 같은 로그를 곳곳에 흩뿌려놓는 경우가 흔히 발생합니다.

우리는 지난 50여 년 동안 모든 UI 작업을 기본적으로 비동기 방식 없이도 수월하게 작업해 왔습니다.

오히려 애플리케이션이 백그라운드에서 무거운 작업을 수행해야 하는 특정 상황에서만 비동기 접근법이 UI 멈춤을 방지하는 데 유용합니다.

예를 들어, 바닐라 자바스크립트에서는 setTimeout(), fetch() 그리고 for await…of 같은 메서드가 비동기적으로 동작하여 호출 시 UI가 멈추지 않습니다.

개발자는 필요한 경우 직접 메서드를 비동기로 만들 수도 있습니다. 이러한 접근 방식은 사용자 경험을 해치지 않으면서도 훨씬 더 쉽고 빠른 개발 과정을 제공합니다.

그러나 React는 여전히 고집스럽게 모든 것을 자바스크립트로 처리합니다. React에는 '정적 HTML'이라는 개념이 없으며, 사용자가 보는 모든 화면은 서버 측(SSR) 또는 클라이언트 측(CSR)에서 생성된 HTML의 결과입니다.

기본 설정이자 가장 일반적인 방식은 클라이언트 측 렌더링(CSR)입니다. 대부분의 개발자는 애플리케이션 성능이 저하될 때까지 이러한 개념을 알지 못하지만, 그때는 이미 늦었고,

React에서 SSR로 전환하는 일은 또 다른 악몽과도 같습니다. 설령 이를 구현하더라도, 단지 비효율을 서버로 옮기는 것에 불과하여 결국 에너지 소비 문제에서 벗어날 수 없습니다.

사용자는 브라우저에서 페이지 소스를 확인하여 애플리케이션이 클라이언트 측 렌더링(CSR)을 사용하는지 알아볼 수 있습니다. 만약 페이지 소스에 실제 콘텐츠가 아닌 템플릿만 표시된다면, 사용자의 기기가 자바스크립트 코드를 실행해 HTML을 생성하고 있다는 뜻입니다.

그렇습니다, 여러분의 배터리 수명이 특별한 이유 없이 소모되고 있을 가능성이 큽니다.

React에서는 HTML처럼 보이는 React 구문 사이에 코드를 처리해야 하는 상황이 자주 발생합니다. 예시는 다음과 같습니다

import React from 'react';

function ExampleComponent({ user }) {
  return (
    <div>
      <h1>Hello, {user.name ? user.name : 'Guest'}!</h1>
      <p>Your last login was {user.lastLogin ? user.lastLogin : 'unknown'}.</p>
    </div>
  );
}

export default ExampleComponent;

이런 혼란을 어떻게 개발환경의 개선됨으로 부를 수 있을까요?

자바스크립트 프레임워크가 없던 시절, 코드는 명확히 세 가지 파일로 분리되었습니다.

HTML, CSS, 그리고 JavaScript. 이 분리는 매우 명확했으며, 심지어 개발자가 모든 것을 HTML에 포함시키고 싶더라도 각 요소는 서로 완전히 분리된 전용 태그를 사용했습니다.

그 시절이 완벽히 이상적이거나 환상적이었던 것은 아니지만, 적어도 현재의 프레임워크들이 강요하는 방식보다는 훨씬 나은 해결책이었습니다.

그러나 잠시 참고 들어주세요, 왜냐하면 이제 겨우 본질의 겉만 건드린 상태니까요.

나는 죽은 package를 보았다.

자바스크립트 프레임워크 광풍은 애플리케이션이 타사 플러그인, 혹은 이들이 부르는 "패키지"의 도움으로 개발된다는 것을 의미합니다.

현대 웹 개발자들은 이러한 패키지에 지나치게 의존하여 기본적인 수학이나 알고리즘조차 잊어버리는 경우가 흔합니다.

그래서 is-oddleft-pad 같은 패키지가 놀라울 정도로 인기를 끌고 있습니다.

이는 마치 조립 라인에서 일하는 공장 노동자처럼, 부품을 만드는 방법은 전혀 모르고 단지 연결하는 법만 아는 상황과 닮아 있습니다.

상황을 더 악화시키는 것은, 자바스크립트의 오래된 한계점들이 최신 버전에서도 여전히 존재한다는 점입니다.

예를 들어, JSON을 파싱할 때 잘못된 문자가 포함되어 있으면, 자바스크립트는 해당 문자의 인덱스를 알려주지 않습니다.
따라서 입력이 길 경우 문제가 되는 문자를 정확히 찾아내기 어려운 상황이 발생합니다. 이런 문제를 해결하려면 또 다른 패키지를 추가해야겠죠?

불편함의 목록이 너무 길어서, Microsoft는 자바스크립트의 일부 한계를 극복하기 위해 TypeScript라는 언어를 개발했습니다.

TypeScript는 자바스크립트를 기반으로 만들어졌지만, 브라우저는 TypeScript를 이해하지 못하기 때문에 애플리케이션을 자바스크립트로 트랜스파일하는 패키지가 필요합니다.

게다가 자바스크립트는 클라이언트 측에서 컴파일되어야 하므로, 브라우저 버전이 최신 자바스크립트 기능과 호환되지 않을 가능성이 있습니다.

따라서 오래된 브라우저에서도 애플리케이션이 제대로 실행되도록 최신 자바스크립트레거시 자바스크립트로 트랜스파일하는 또 다른 단계가 필요합니다.

예상할 수 있듯이, 각 반복 과정은 코드를 점점 더 비대하게 만듭니다. 이러한 끝없는 비효율성의 결과로, 단순한 애플리케이션조차 수천 개의 직접적인 의존성과 그에 따른 수천 개의 간접 의존성을 가지게 되어, 엄청나게 큰 자바스크립트 파일이 생성됩니다.

이 모든 파일들은 결국 클라이언트 머신으로 전송되어 다운로드, 파싱, 컴파일, 실행 과정을 거쳐 단지 HTML을 생성하게 되는데, 사실 그게 최종적으로 필요한 전부입니다. 정말 미친 상황이죠.

웹 개발자가 프로젝트를 체크아웃한 후 npm install을 실행하면, 보통 node_modules 하위 폴더에 대량의 패키지가 다운로드됩니다. 여기서 중요한 점은, 이러한 패키지의 주요 버전이 프로젝트의 package.json 파일에 하드코딩되어 있다는 것입니다.

이 부분을 조심스럽게 다뤄야 합니다. 왜냐하면 매우 불안정하기 때문입니다! 이는 웹 애플리케이션의 의존성에는 사용자 시스템을 최신 상태로 유지해야 한다는 압력이 적용되지 않음을 의미합니다. 개발자가 매주 수동으로 각 의존성 업데이트를 확인하지 않는 이상 말입니다. 하지만, 당연히 그런 일은 거의 일어나지 않죠.

게다가 각 의존성마다 또 다른 의존성을 가지고 있을 수 있어서, 패키지 폴더 안에 또 다른 node_modules 하위 폴더가 생기고, 그 폴더 안에도 다시 의존성들이 존재할 수 있습니다.
이러한 요소들이 로드되는 순서에 따라, 동일한 패키지의 혼합된 버전이 동시에 실행되면서 프로젝트가 깨질 수도 있습니다. 물론, 이러한 패키지 문제를 해결하기 위한 패키지들이 존재하지만, 당연히 이들 역시 다른 문제를 일으켜 결국 또 다른 패키지가 필요하게 됩니다. 이것은 말 그대로 혼돈의 재귀 머신이며, 단순한 ‘Hello World’ 프로젝트조차 4만 개가 넘는 파일을 필요로 하게 만듭니다. 이걸 그들은 "재미"라고 부릅니다.

하지만, 상황은 더 나빠집니다. 일부 의존성은 Node.js, Java, yarn과 같은 시스템 관련 요소인데, 개발자가 운영 체제를 업데이트하면 애플리케이션이 깨질 수 있습니다.

이것이 바로 Docker가 그렇게 인기를 끄는 이유 중 하나입니다. Docker는 특정 프로젝트에 맞는 매우 구체적인 운영 체제 상태를 컨테이너에 고정시켜 줍니다. 결국 문제가 발생했을 때, 근본 원인을 고치기보다는 새로운 도구를 만들어 우회하는 것이 현대 웹의 방식이죠. 이게 바로 "현대 웹 개발"을 요약한 모습입니다.

대표적인 사례로는 GitHub의 Blame 페이지에서 볼 수 있습니다.

이 페이지는 React 기반의 '뷰포트 최적화'를 구현했는데, 이로 인해 브라우저 검색 기능을 사용해 뷰포트 밖의 내용을 검색하면 결과가 비어 있게 됩니다.

그럼 이 "천재들"은 무엇을 했을까요? React와 쓸모없는 npm 패키지들로 구동되는 자체 검색 기능을 만들어냈습니다.

그 결과, 최소화된 JavaScript 코드만 해도 거의 3MB에 달합니다.
정적 HTML만으로도 중급 스마트폰에서도 수천 개의 행이 있는 대화형 테이블을 쉽게 렌더링할 수 있다는 사실을 그들이 알기만 했다면 말입니다.

거북이 보단 빠르겠지, 혹은 아니거나..

2015년 발표에서 React의 핵심 개발자 중 한 명인 Tom Occhino는 다음과 같이 말했습니다.

사람들이 React를 내부적으로 사용해 보기 시작했는데, 모두 같은 반응을 보였습니다:
“좋아요, 이게 성능이 충분히 좋을지는 전혀 모르겠지만, 너무 재미있어서 성능이 느려도 상관없어요 — 누군가가 더 빠르게 만들어주겠죠.”

React 팀은 자바스크립트에서 추상화가 많아질수록 성능이 나빠진다는 사실을 모두 알고 있었습니다.

그들은 단지 누군가 마법 같은 해결책을 내놓기를 바랐지만, 우리가 알다시피 그런 일은 한 번도 일어나지 않았고 앞으로도 없을 가능성이 높습니다.

2018년으로 넘어가면, Netflix는 포털을 React에서 바닐라 자바스크립트로 마이그레이션했다고 발표했습니다. 이를 통해 JavaScript 번들 크기를 1/5로 줄이고, 로딩 시간을 50% 단축하는 데 성공했습니다

Material UI는 React 생태계에서 매우 인기 있는 라이브러리로, 주당 400만 회 이상 다운로드됩니다.

그러나 이는 React 생태계에서 발생하는 성능 문제를 보여주는 또 다른 사례입니다. 다음은 모바일 모드에서 단순히 버튼들(동일 스타일)을 로드하는 화면에서 JavaScript 처리에 소요된 총 시간입니다.

JavaScript 로드 및 실행 시간이 지나치게 길어 성능이 크게 저하됩니다.

이 사례는 React 기반 프로젝트가 종종 단순한 UI에도 불구하고 과도한 JavaScript 실행으로 인해 느려질 수 있다는 점을 명확히 보여줍니다.

Material UI와 같은 라이브러리를 사용하는 경우, 라이브러리 자체의 무거운 구조가 UI 반응성과 로드 시간을 심각하게 저하시킬 수 있습니다.

이처럼 React 생태계의 성능 문제는 단순한 컴포넌트에서도 쉽게 드러납니다.

일부 사람들은 "누가 단일 페이지에 수천 개의 버튼을 넣겠느냐"는 식의 논리로 문제의 본질을 흐리려 할 것입니다.

하지만 이는 핵심을 놓친 주장입니다. 인기 있는 React 패키지들은 이미 느린 React 자체와 비교해도 극도로 비효율적입니다.

반면, 동일한 결과를 얻기 위해 정적 HTML을 사용하면 JavaScript가 전혀 필요 없기 때문에 처리 시간이 0ms에 불과합니다. 이 비교는 React 생태계의 성능 문제를 명확히 드러내며, 단순히 버튼 수가 많고 적음의 문제가 아니라 기본적인 설계 효율성의 차이임을 보여줍니다.

Material UI가 초래하는 복잡성 증가는 JavaScript 처리에만 영향을 미치는 것이 아닙니다. 최종적으로 생성되는 HTML도 영향을 받아, DOM 요소의 개수는 두 배로 증가하고, 파일 크기는 10배 커집니다!
그러나 사용자 관점에서 보면, 이 테스트에서 Material UI와 순수 React는 겉보기에는 완전히 동일하게 보입니다.

React Native에서는 더 심각한 성능 문제가 드러납니다. 만약 FlatList 컴포넌트에 단순한 텍스트가 포함된 50개 이상의 항목이 있다면, 성능이 크게 저하됩니다. 이 상황에서 React는 다음과 같은 경고를 띄웁니다:

VirtualizedList: You have a large list that is slow to update — make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc.

즉, React는 이러한 성능 문제를 개발자 탓으로 돌리며, 최적화된 컴포넌트 사용을 요구합니다. 그러나 근본적으로, 기본 텍스트 항목 몇 개조차 처리하지 못하는 구조적인 문제를 보여주는 사례입니다.

React는 스스로에 대한 존중이 부족한 수준에 이르러, 코드에 버그가 있을 수 있다고 인정하고, 심지어 절대 위치 설정 같은 기본적인 개념조차 "복잡하다"고 칭합니다. 정말요?! 이게 한심하지 않게 들린다면, 다른 관점에서 접근해보죠.

1997년, Id Software는 Quake 2를 출시했습니다. 이 3D 가속 게임은 당시의 기기에서 초당 60프레임을 구현할 수 있었습니다. 이는 매 16ms마다 엔진이 다음과 같은 작업을 처리해야 했다는 것을 의미합니다:

  • 수백 개의 폴리곤과 텍스처 처리
  • 동적 조명
  • AI와 물리 연산
  • 애니메이션
  • 다중 입력 장치 지원
  • 다중 채널 오디오
  • I/O 작업
  • 네트워크 동기화
  • HUD (헤드업 디스플레이) 등

이 모든 것을 동시에 처리하는 Quake 2는 오늘날 우리가 보는 대부분의 웹 애플리케이션보다 압도적으로 복잡한 애플리케이션입니다.

비교하자면, React 기반의 많은 애플리케이션은 상대적으로 단순한 작업조차도 제대로 최적화하지 못하는 경우가 많습니다. Quake 2 같은 수준의 복잡성과 성능을 보면, 현재 웹 개발이 얼마나 느리고 비효율적인지를 다시 생각하게 됩니다.

오늘날 우리는 1997년의 컴퓨터보다 수백 배 더 빠른 기기를 가지고 있지만, 정적 테이블 하나를 보여주는 웹사이트조차 JavaScript 코드로 HTML을 생성하는 데 몇 초가 걸립니다.

대체 무엇이 잘못되었기에 이런 상황을 만들어냈을 뿐만 아니라, 더 심각하게도 이 비효율성을 당연하게 받아들이게 된 걸까요?

현재의 웹 애플리케이션은 적어도 Quake 2가 실행되던 당시의 컴퓨터에서 Quake 2보다 100배 더 빠르게 동작해야 마땅합니다.

그러나 현실은, 기본적인 작업조차도 과도한 추상화와 비효율적인 설계로 인해 극도로 느리고 비대해진 상태입니다. 이는 성능 향상보다는 복잡성을 정당화하고, 기술의 진보를 남용한 결과입니다.

개발자들은 HTML과 CSS가 얼마나 빠르게 파싱되고 렌더링될 수 있는지 잘 모르는 것 같습니다. 웹이 느린 이유는 HTML과 CSS 때문이 아닙니다. 물론, 개발자가 어처구니없는 실수를 하지 않는 한 말이죠.

심지어 바닐라 JavaScript도 적절히 사용하면 꽤 좋은 성능을 발휘할 수 있습니다. 예를 들어, JSON 데이터 1,000개를 파싱하고 이를 HTML 테이블에 채우는 작업은 1초의 아주 작은 일부분 안에 처리될 수 있습니다.

이 과정은 너무 빨라서 JSON을 다운로드한 이후에는 비동기 호출이 전혀 필요하지 않을 정도입니다. 이는 우리가 사용하는 많은 도구들이 불필요한 복잡성을 더하고 있을 뿐, 기본 기술만으로도 충분히 뛰어난 성능을 낼 수 있음을 보여줍니다.

웹 성능 저하를 피하기 위해 개발자들이 갈망해 온 모든 최적화는, 대부분 비대한 프레임워크와 그 패키지들의 비효율성에서 비롯된 것입니다. 현대 도구들이 기본적으로 엉망인 결과물을 제공하고, 개발자가 원하는 결과를 얻기 위해 우회 방법을 찾아야 한다는 개념을 정상화했다는 점은 정말 말도 안 되는 일입니다.
이는 마치 자동차 제조사가 정비사들에게 이렇게 말하는 것과 같습니다

"엔진을 최적화해서 고객의 자동차가 제대로 작동하도록 하세요."

게다가, 직면한 문제를 해결하기 위해 외부 도움을 구하려 하면 애플리케이션이 사용하는 도구와 버전의 조합에 따라 적용 가능한 답변이 달라지기 때문에 문제가 더 복잡해집니다. 이러한 조합이 너무 많아서, 당신의 상황이 고유한 경우일 가능성이 높습니다.

이로 인해 흔히 볼 수 있는 댓글이 다음과 같습니다:

"답변 A는 저에게는 효과가 없었지만, 답변 B는 효과가 있었습니다."

이러한 상황은 현대 웹 개발 환경이 얼마나 복잡하고, 문제 해결이 얼마나 비효율적으로 이루어지는지를 보여줍니다.

REFERENCE

https://medium.com/@fulalas/web-crap-has-taken-control-71c459df6e62

profile
모르는것은 그때그때 기록하기

0개의 댓글