[번역] 리액트, 널 사랑하지만 넌 나를 슬프게 해

eunbinn·2022년 10월 3일
94

FrontEnd 번역

목록 보기
13/38
post-thumbnail

출처: https://marmelab.com/blog/2022/09/20/react-i-love-you.html

React.js에게,

우리가 함께한 지 어언 10년이라는 시간이 흘렀습니다. 많이 발전했네요. 그런데 이제는 감당이 안 됩니다. 우린 대화가 필요해요.

창피한 거 알아요. 아무도 그런 대화를 하고 싶어 하지 않죠. 그러니 대신 노래로 해볼게요.

그건 바로 너였어 (You Were The One)

저에게 자바스크립트가 첫사랑은 아닙니다. 당신 이전에 jQuery, Backbone.js, Angular.js와 오랜 모험을 했죠. 나는 자바스크립트 프레임워크와의 관계에서, 더 나은 UI, 더 높은 생산성, 그리고 더 부드러운 개발자 경험을 기대했습니다. 하지만 프레임워크의 사고방식에 맞게 내 코드에 대한 생각 또한 끊임없이 바꿔야 하는 좌절감도 알고 있었죠.

당신을 만났을 때, 저는 Angular.js와의 오랜 관계에서 막 벗어난 참이었습니다. 저는 scope는 말할 것도 없고 watchdigest에 지쳐 있었죠. 저는 혼란스럽게 만들지 않는 프레임워크를 찾고 있었습니다.

저는 첫눈에 반했어요. 당신의 단방향의 데이터 바인딩은 제가 그 당시 알고 있던 모든 것을 통틀어 매우 신선했습니다. 데이터 동기화와 성능에 대해 가지고 있던 모든 종류의 문제는 당신과 함께라면 존재하지 않았죠. HTML 요소에서 문자열로 표현된 어설픈 모방이 아닌 순수한 자바스크립트였습니다. 모두가 너무 아름다워 계속 쳐다보게 만든 "선언적 컴포넌트"라는 걸 가지고 계셨죠.

image

당신은 쉬운 타입이 아니었어요. 당신과 잘 지내기 위해서 코딩 습관을 길러야 했지만, 저는 그럴만한 가치가 있다고 생각했죠. 초반에 저는 당신과 함께 있어 너무 행복하다고 모든 사람들에게 당신에 대해 계속 말했습니다.

폼의 새로운 영웅 (Heroes Of New Forms)

폼을 처리하면서부터 상황은 조금씩 이상해지기 시작했습니다. 바닐라 자바스크립트에서 폼과 입력은 다루기 어렵습니다. 하지만 리액트와 함께 있으면, 훨씬 더 힘들어집니다.

먼저 개발자는 제어된 입력과 제어되지 않는 입력 중 하나를 선택해야 합니다. 두 가지 모두 엣지 케이스에 대한 단점과 버그가 존재합니다. 그런데 애초에 왜 선택을 해야하는거죠? 두 가지 형태의 전략은 너무 많습니다.

"권장하는" 방법인 제어된 컴포넌트는 매우 복잡합니다. 폼 하나를 추가하기 위해 아래와 같은 코드가 필요하죠.

import React, { useState } from "react";

export default () => {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);

  function handleChangeA(event) {
    setA(+event.target.value);
  }

  function handleChangeB(event) {
    setB(+event.target.value);
  }

  return (
    <div>
      <input type="number" value={a} onChange={handleChangeA} />
      <input type="number" value={b} onChange={handleChangeB} />

      <p>
        {a} + {b} = {a + b}
      </p>
    </div>
  );
};

만약 두 가지 방법만 있었다면 그나마 나았을겁니다. 하지만 기본값, 유효성 검사, 종속 입력, 에러 메세지 등 실무에서 폼을 만드는 데에는 엄청난 양의 코드가 필요하기 때문에 서드파티 폼 프레임워크를 사용해야 합니다. 그리고 그들은 모두 어떤 방면에서 조금씩 부족합니다.

  • Redux-form은 Redux를 사용할 때에 자연스러운 선택처럼 보였지만, 리드 개발자가 프로젝트를 포기했습니다.
  • React-final-form은 고쳐지지 않은 버그들로 가득하며, 이 또한 리드 개발자가 포기했습니다. 그래서 저는 다른 프레임워크인,
  • Formik을 살펴봤고, 대중적이지만 무겁고, 폼이 클 경우 느리며 기능이 다소 제한적입니다. 따라서 우리는
  • React-hook-form을 사용하기로 했습니다. 빠르지만 숨겨진 버그들이 있고 문서가 미로처럼 난해하지만요.

리액트로 몇 년 동안 폼을 만들었지만, 저는 여전히 읽기 쉬운 코드로 강력한 사용자 경험을 만드는 데 어려움을 겪고 있습니다. Svelte가 폼을 다루는 방식을 보면 제가 잘못된 추상화에 얽매여 있음을 느낄 수 있습니다. 스벨트로 만든 아래의 폼을 확인해보세요.

<script>
  let a = 1;
  let b = 2;
</script>

<input type="number" bind:value="{a}" />
<input type="number" bind:value="{b}" />

<p>{a} + {b} = {a + b}</p>

당신은 너무 Context에 예민해요 (You're Too Context Sensitive)

우리가 처음 만난지 얼마 되지 않아, 당신은 나에게 강아지 Redux를 소개시켜주었습니다. Redux 없이 당신은 아무 데도 갈 수 없었죠. 처음에는 별로 신경쓰지 않았어요, 귀여웠거든요. 하지만 곧 저는 세계가 Redux를 중심으로 돌고 있다는 것을 깨달았죠. 또한 Redux는 프레임워크를 만들기 더 어렵게 만들었습니다. 다른 개발자들은 기존 리듀서로 앱을 쉽게 조정할 수 없었죠.

당신도 알아챘고, Redux를 제거하고 자체적으로 제공하는 useContext를 사용하고자 했습니다. useContext는 Redux의 중요한 기능인 컨텍스트 일부의 변화에 반응하는 능력은 없었습니다. 이 둘은 성능의 관점에서 너무나도 달랐습니다.

// Redux
const name = useSelector((state) => state.user.name);
// React context
const { name } = useContext(UserContext);

첫번째 예시에서 컴포넌트는 user name이 바뀔 경우에만 리렌더링 됩니다. 두번째 예시에서는 user의 어떤 부분이 바뀌더라도 리렌더링 됩니다. 이것은 매우 중요하며, 불필요한 리렌더링을 피하기 위해 컨텍스트를 분리해야 합니다.

// 미친 짓이지만, 이외 방법이 없습니다
export const CoreAdminContext = (props) => {
  const {
    authProvider,
    basename,
    dataProvider,
    i18nProvider,
    store,
    children,
    history,
    queryClient,
  } = props;

  return (
    <AuthContext.Provider value={authProvider}>
      <DataProviderContext.Provider value={dataProvider}>
        <StoreContextProvider value={store}>
          <QueryClientProvider client={queryClient}>
            <AdminRouter history={history} basename={basename}>
              <I18nContextProvider value={i18nProvider}>
                <NotificationContextProvider>
                  <ResourceDefinitionContextProvider>
                    {children}
                  </ResourceDefinitionContextProvider>
                </NotificationContextProvider>
              </I18nContextProvider>
            </AdminRouter>
          </QueryClientProvider>
        </StoreContextProvider>
      </DataProviderContext.Provider>
    </AuthContext.Provider>
  );
};

image

당신에게 성능 문제가 있을 때, 대부분 컨텍스트가 너무 크기 때문이라서 저는 컨텍스트를 나눌 수 밖에 없었어요.

저는 useMemouseCallback을 사용하고 싶지 않습니다. 쓸모없는 리렌더링은 당신의 문제이지 문제가 아니에요. 하지만 당신은 제게 사용하도록 강요합니다. 성능이 좋은 간단한 폼 입력을 만들기 위해서 어떻게 해야하는지 아래 코드를 보세요.

// 출처:  https://react-hook-form.com/advanced-usage/#FormProviderPerformance
const NestedInput = memo(
  ({ register, formState: { isDirty } }) => (
    <div>
      <input {...register("test")} />
      {isDirty && <p>This field is dirty</p>}
    </div>
  ),
  (prevProps, nextProps) =>
    prevProps.formState.isDirty === nextProps.formState.isDirty
);

export const NestedInputContainer = ({ children }) => {
  const methods = useFormContext();

  return <NestedInput {...methods} />;
};

10년이 지났는데도 당신은 여전히 그 결점을 가지고 있습니다. useContextSelector를 제안하는게 얼마나 어려울지 보이네요.

물론 당신도 잘 알고 있을 겁니다. 가장 중요한 문제가 성능에서의 병목임에도 불구하고 당신은 다른 문제에만 집중하고 있어요.

나는 이 중 어느 것도 원하지 않아요 (I Want None Of This)

당신은 제게 DOM 노드에 직접 접근해서는 안 된다고 얘기했습니다. DOM이 더럽다고 생각한 적은 없지만, 당신을 방해했기 때문에 저는 하지 않았습니다. 이제 저는 당신이 요청한대로 refs를 사용합니다.

하지만 이 ref는 바이러스처럼 퍼집니다. 대부분의 경우 컴포넌트가 ref를 사용하면 하위 컴포넌트로 전달합니다. 두 번째 컴포넌트가 리액트 컴포넌트인 경우 트리의 한 컴포넌트가 HTML 요소를 렌더링할 때까지 ref를 다른 컴포넌트로 전달해야 합니다. 따라서 ref를 전달하는 코드들로 가득하게되고 프로세스의 가독성을 감소시킵니다.

ref 전달은 다음과 같이 간단합니다.

const MyComponent = (props) => <div ref={props.ref}>Hello, {props.name}!</div>;

아닙니다, 그건 너무 쉽네요. 당신은 대신 혐오스러운 react.forwardRef를 만들었습니다.

const MyComponent = React.forwardRef((props, ref) => (
  <div ref={ref}>Hello, {props.name}!</div>
));

왜 이렇게 복잡한지 묻는다면, 단순히 forwardRef로는 제네릭 컴포넌트(타입스크립트의 관점에서)를 만들 수 없기 때문입니다.

// 이를 어떻게 forwardRef 할 수 있을까요?
const MyComponent = <T>(props: <ComponentProps<T>) => (
    <div ref={/* pass ref here */}>Hello, {props.name}!</div>
);

게다가 당신은 refs가 DOM 노드 뿐만 아니라 함수 컴포넌트의 this도 될 수 있다고 정했습니다. 다르게 얘기하면 "리렌더링을 트리거하지 않는 상태"입니다. 제 경험에 따르면 ref를 사용하는 이유는 당신 때문이었습니다. 당신의 useEffect API가 너무 이상하기 때문에요. 즉 refs는 당신이 만든 문제에 대한 해결책입니다.

나비효과 (The Butterfly (use) Effect)

useEffect에 대해 말하자면 저는 개인적으로 문제가 있습니다. mount, unmount와 이벤트 업데이트를 모두 포함하는 하나의 통합된 API로 우아한 혁신이라고 알고 있습니다. 하지만 이것이 어떻게 발전했다고 여겨질 수 있죠?

// lifecycle callback과 함께
class MyComponent {
  componentWillUnmount: () => {
    // do something
  };
}

// useEffect와 함께
const MyComponent = () => {
  useEffect(() => {
    return () => {
      // do something
    };
  }, []);
};

이 한줄 만으로도 당신의 useEffect에 대한 나의 슬픔을 표현할 수 있습니다.

    }, []);

제 코드 곳곳에 카발리스틱 기호들을 볼 수 있는데 이들은 모두 useEffect 때문입니다. 게다가 당신은 저에게 아래와 같이 의존성을 추적하도록 강요했죠.

// 데이터가 없으면 page를 바꿉니다
useEffect(() => {
  if (
    query.page <= 0 ||
    (!isFetching && query.page > 1 && data?.length === 0)
  ) {
    // 존재하지 않는 페이지에 대한 쿼리, 페이지를 1로 세팅합니다
    queryModifiers.setPage(1);
    return;
  }
  if (total == null) {
    return;
  }
  const totalPages = Math.ceil(total / query.perPage) || 1;
  if (!isFetching && query.page > totalPages) {
    // 범위를 벗어난 페이지에 대한 쿼리, 페이지를 마지막 기존 페이지로 세팅합니다
    // 마지막 페이지의 마지막 요소를 제거했을 경우에 발생합니다
    queryModifiers.setPage(totalPages);
  }
}, [isFetching, query.page, query.perPage, data, queryModifiers, total]);

마지막 줄 보이시죠? 모든 반응형 변수들을 의존성 배열에 포함시켜야 했습니다. 저는 참조 카운팅은 가비지 컬렉터가 있는 모든 언어에서 기본으로 가지고 있는 특징이라고 생각했습니다. 하지만 아니요, 저는 스스로 의존성을 미세하게 관리해야 했습니다. 당신이 관리할 줄 모르기 때문이죠.

image

그리고 종종 의존성 중 하나는 제가 만든 함수였습니다. 변수와 함수의 차이를 만들지 않기 때문에 useCallback을 사용하여 아무것도 리렌더링하지 않을 것을 알려줘야 했습니다. 동일한 결과, 동일한 마지막 줄의 기호를 볼 수 있죠.

const handleClick = useCallback(
  async (event) => {
    event.persist();
    const type =
      typeof rowClick === "function"
        ? await rowClick(id, resource, record)
        : rowClick;
    if (type === false || type == null) {
      return;
    }
    if (["edit", "show"].includes(type)) {
      navigate(createPath({ resource, id, type }));
      return;
    }
    if (type === "expand") {
      handleToggleExpand(event);
      return;
    }
    if (type === "toggleSelection") {
      handleToggleSelection(event);
      return;
    }
    navigate(type);
  },
  [
    // 제발 그만
    rowClick,
    id,
    resource,
    record,
    navigate,
    createPath,
    handleToggleExpand,
    handleToggleSelection,
  ]
);

몇 개의 이벤트 핸들러와 라이프사이클 콜백이 있는 간단한 컴포넌트도 이 의존성 지옥을 관리해야 한다는 이유만으로 말도 안되는 코드 더미가 됩니다. 이 모든 것은 컴포넌트가 임의의 횟수만큼 실행될 수 있다고 당신이 결정했기 때문입니다.

예를 들어, 매 초마다 그리고 사용자가 버튼을 클릭할 때마다 증가하는 카운터를 만든다면 아래와 같이 해야합니다.

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount((count) => count + 1);
  }, [setCount]);

  useEffect(() => {
    const id = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [setCount]);

  useEffect(() => {
    console.log("The count is now", count);
  }, [count]);

  return <button onClick={handleClick}>Click Me</button>;
}

만약 당신이 의존성을 관리할 수 있었다면 아래처럼 간단히 쓸 수 있었겠죠.

function Counter() {
  const [count, setCount] = createSignal(0);

  const handleClick = () => setCount(count() + 1);

  const timer = setInterval(() => setCount(count() + 1), 1000);

  onCleanup(() => clearInterval(timer));

  createEffect(() => {
    console.log("The count is now", count());
  });

  return <button onClick={handleClick}>Click Me</button>;
}

참고로 이 코드는 유효한 Solid.js 코드입니다.

image

마지막으로, useEffect를 현명하게 사용하려면 53페이지 분량의 논문을 읽어야 합니다. 정말이지, 훌륭한 문서입니다. 하지만 라이브러리가 제대로 사용하기 위해 수십 페이지를 확인할 것을 요구한다면, 그것은 잘 설계되지 않았다는 뜻이 아닐까요?

핵심을 개선해요 (Makeup Your Mind)

useEffect의 부족한 추상화에 대해 얘기했으니, 당신은 이를 개선하려고 노력했습니다. 당신은 useEvent, useInsertionEffect, useDeferredValue, useSyncWithExternalStore 및 다른 속임수들을 알려주었죠.

그리고 이들은 실제로 당신을 아름답게 만들었습니다.

function subscribe(callback) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe, // 리액트는 동일한 함수를 넘기지 않는 이상 재구독 하지 않습니다
    () => navigator.onLine, // 클라이언트에서 값을 얻는 방법
    () => true // 서버에서 값을 얻는 방법
  );
}

하지만 저에게는 호박에 줄긋기일 뿐입니다. 만약 반응형 effects가 사용하기 쉬웠다면 이 훅들도 필요 없었겠죠.

달리 말하면, 핵심 API를 점점 더 성장시키는 것 외에는 다른 해결책이 없습니다. 거대한 코드베이스를 유지해야 하는 저 같은 사람들에게 이러한 지속적인 API의 증가는 악몽입니다. 당신이 매일 메이크업을 더 짙게 할수록 감추려고 하는 것이 무엇인지 계속 상기시킬 뿐입니다.

엄격해요 (Strict Machine)

당신의 훅은 좋은 생각이지만, 비용이 따릅니다. 이 비용은 훅의 규칙입니다. 이들은 외우기도 쉽지 않고, 실행에 옮기기도 쉽지 않습니다. 하지만 그들은 제게 필요하지 않은 코드에 시간을 보내도록 강요합니다.

예를 들면, 엔드 유저가 어디든 드래그 할 수 있는 "inspector" 컴포넌트가 있다고 해봅시다. 사용자는 inspector를 숨길 수도 있습니다. 숨겨진 경우 inspector 컴포넌트는 아무것도 렌더링되지 않습니다. 따라서 저는 "일찍 리턴하고" 싶고, 의미 없는 이벤트 리스너 등록을 피하고 싶습니다.

const Inspector = ({ isVisible }) => {
  if (!isVisible) {
    // 일찍 리턴
    return null;
  }
  useEffect(() => {
    // 이벤트 리스너 등록
    return () => {
      // 이벤트 리스너 해제
    };
  }, []);
  return <div>...</div>;
};

하지만 아니요, useEffect 훅은 props에 따라 싫행될 수도 있고 되지 않을 수도 있기 때문에 훅의 규칙을 위반합니다. 대신 저는 모든 effects에 조건을 추가하여 isVisible prop이 false인 경우에 일찍 리턴하도록 해야합니다.

const Inspector = ({ isVisible }) => {
  useEffect(() => {
    if (!isVisible) {
      return;
    }
    // 이벤트 리스너 등록
    return () => {
      // 이벤트 리스너 해제
    };
  }, [isVisible]);

  if (!isVisible) {
    // 그닥 이르지 않게 리턴
    return null;
  }
  return <div>...</div>;
};

결과적으로 모든 effects는 isVisible prop을 의존성 배열에 포함하게 되며 잠재적으로 너무 자주 실행될 수 있습니다(성능을 저하시킬 수 있습니다). 알아요, isVisible이 false면 아무 것도 렌더링하지 않는 중간 컴포넌트를 만들어야 하죠. 하지만 왜 그래야하나요? 이것은 훅의 규칙이 저를 방해하는 하나의 예에 물과합니다. 다른 많은 예시들이 있죠. 한 가지 분명한 것은 제 코드의 상당 부분이 훅의 규칙을 충족시키는 데에 사용된다는 것입니다.

훅의 규칙은 구현 세부 사항의 결과입니다. 당신이 훅을 구현하기 위해 결정한 것이죠. 하지만 꼭 그렇게 될 필요는 없습니다.

너무 오래되었어요 (You've Been Gone Too Long)

당신은 2013년부터 존재해 왔으며, 가능한 한 하위 호환성을 유지하려고 노력했습니다. 그리고 그것에 대해 저는 감사합니다. 이것이 당신과 함께 거대한 코드를 만들 수 있었던 이유 중 하나입니다. 하지만 이런 하위 호환성에는 대가가 따릅니다. 문서 및 커뮤니티 리소스들은 잘해봐야 구식이고 최악의 경우엔 잘못되어 있을 가능성이 있습니다.

예를 들어 StackOverFlow에서 "리액트 마우스 위치"를 검색하면, 첫 번째 결과는 이미 한 세기 전의 구식 리액트인 아래와 같은 솔루션을 제안합니다.

class ContextMenu extends React.Component {
  state = {
    visible: false,
  };

  render() {
    return (
      <canvas
        ref="canvas"
        className="DrawReflect"
        onMouseDown={this.startDrawing}
      />
    );
  }

  startDrawing(e) {
    console.log(
      e.clientX - e.target.offsetLeft,
      e.clientY - e.target.offsetTop
    );
  }

  drawPen(cursorX, cursorY) {
    // 라밸에 도면 정보를 표시하는 데에만 사용됩니다
    this.context.updateDrawInfo({
      cursorX: cursorX,
      cursorY: cursorY,
      drawingNow: true,
    });

    // 무엇인가 그립니다
    const canvas = this.refs.canvas;
    const canvasContext = canvas.getContext("2d");
    canvasContext.beginPath();
    canvasContext.arc(
      cursorX,
      cursorY /* 시작 지점 */,
      1 /* 반지름 */,
      0 /* 시작 각도 */,
      2 * Math.PI /* 끝 각도 */
    );
    canvasContext.stroke();
  }
}

우웩

특정 리액트 기능에 대한 npm 패키지를 찾을 때, 저는 대부분 오래되고 구식 문법을 가진 버려진 패키지를 찾게됩니다. react-draggable을 예로 들어봅시다. 리액트로 드래그 앤 드롭을 구현하기 위한 사실상의 표준입니다. 많은 이슈들이 열려있고 개발 활동 빈도가 낮습니다. 아마 이는 여전이 클래스 컴포넌트 기반이기 때문일 것입니다. 코드베이스가 너무 오래되면 컨트리뷰터를 끌어들이기 어렵죠.

당신의 공식 문서에서는 여전히 useEffect 대신 componentDidMountcomponentWillUnmount를 쓸 것을 권장하고 있습니다. 핵심 팀은 지난 2년 동안 Beta docs라는 새로운 버전을 개발해왔습니다. 그들은 아직도 이를 공개할 준비가 되지 않았습니다.

전반적으로 훅으로의 기이이이인 마이그레이션은 여전히 끝나지 않았고, 이는 리액트 커뮤니티에서 분열을 초래했습니다. 새로운 개발자들은 리액트 생태계에서 그들의 길을 찾기 위해 고군분투하고, 오래된 개발자들은 최신 개발에 뒤지지 않기 위해 노력합니다.

가족 문제 (Family Affair)

처음에, 당신의 아버지인 Facebook은 정말 멋져보였습니다. Facebook은 "사람들을 더 가까이 모이도록"하고 싶었죠. 부모님을 뵐 때마다 새로운 친구들을 만났습니다.

하지만 일은 점점 엉망이 되었습니다. 당신의 부모님은 군중을 조작하기 시작했죠. "가짜 뉴스"의 개념을 발명하고 동의 없이 모든 사람의 파일을 보관하기 시작했습니다. 몇 년 전 저는 Facebook 계정을 삭제했을 정도로 당신의 부모님을 뵈는 것이 무서워졌습니다.

알아요, 부모의 행동에 대해 자식에게 책임을 물을 수 없죠. 하지만 당신은 여전히 그들과 함께 살아요. 그들의 발전에 자금을 대죠. 그들은 당신의 가장 큰 사용자입니다. 당신은 그들에게 의존합니다. 어느 날 그들의 행동 때문에 그들이 무너지면, 당신은 그들과 함께 무너질거에요.

다른 주요한 자바스크립트 프레임워크는 그들의 부모로부터 자유로워졌습니다. 그들은 독립했고 OpenJS Foundation이라고 불리는 재단에 가입했습니다. Node.js, Electron, webpack, loadsh, eslint, 그리고 심지어 Jest도 이제 기업과 개인들로부터 자금을 조달받습니다. 그들이 할 수 있기에 당신도 할 수 있어요. 하지만 당신은 하지 않죠. 부모님과 붙어있어요. 왜죠?

image

내 탓이 아니에요, 당신 잘못이죠 (It's Not Me, It's You)

당신과 나는 같은 삶의 목표를 가지고 있어요. 개발자들이 더 나은 UI를 만들 수 있도록 돕죠. 저는 React-admin을 통해 하고 있습니다. 그래서 당신의 도전과 겪어야만 했던 트레이드오프들을 알아요. 당신의 일은 결코 쉽지 않고 아마 제가 모르는 수많은 문제들을 해결하고 있을 겁니다.

하지만 저는 끊임없이 당신의 결점을 숨기려고 노력하는 저 자신을 발견합니다. 당신에 대해 이야기할 때, 저는 결코 위 문제들을 언급하지 않습니다. 우리가 구름 한 점 없는 멋진 커플인 척하죠. react-admin에서는 당신을 직접 사용해야 하는 번거로움을 없앤 API들을 소개합니다. 그리고 사람들이 react-admiin에 대해 불평할 때, 저는 그들의 문제를 해결하기 위해 최선을 다합니다. 대부분의 경우, 그들은 당신에게 문제를 가지고 있습니다. 프레임워크 개발자로서 저 또한 최전선에 있습니다. 저는 모든 문제를 다른 사람들보다 먼저 알게 되죠.

다른 프레임워크들도 살펴봤습니다. 그들은 그들만의 결점이 있어요. Svelte는 자바스크립트가 아니고 SolidJS는 아래와 같은 불쾌한 함정들이 있죠.

// SolidJS에서 동작합니다
const BlueText = (props) => <span style="color: blue">{props.text}</span>;

// SolidJS에서 동작하지 않습니다
const BlueText = ({ text }) => <span style="color: blue">{text}</span>;

하지만 그들은 당신의 결점을 가지고 있지 않아요. 가끔 울고싶게 만드는 결점들. 몇 년 동안 그것들을 다루다 보면 너무 성가시게 되는 결점들. 다른 프레임워크를 시도하고 싶게 만드는 결점들. 이에 비해 다른 모든 프레임워크는 신선하죠.

당신을 끊을 수 없어요 (I Can't Quit You Baby)

문제는, 그럼에도 불구하고, 당신을 떠날 수 없다는 겁니다.

먼저, 저는 당신의 친구들을 사랑해요. MUI, Remix, react-query, react-testing-library, react-table... 그들과 함께 있을 때 저는 멋진 것들을 할 수 있습니다. 그들은 저를 더 나은 개발자가 되게 합니다. 더 나은 사람으로 만듭니다. 그들을 떠나지 않고는 당신을 떠날 수 없어요.

생태계가 문제야, 바보야

최고의 커뮤니티와 최고의 서드파티 모듈을 보유하고 있다는 것을 부정할 수 없습니다. 하지만 솔직히, 개발자들이 당신의 퀄리티 때문이 아니라 당신의 생태계의 퀄리티 때문에 당신을 선택한다는 것은 유감입니다.

둘째, 저는 당신에게 너무 많은 투자를 했습니다. 저는 당신과 함께 거대한 코드베이스를 구축했기에 뒤짚어 엎지 않고서는 다른 프레임워크로 옮길 수 없습니다. 저는 지속 가능한 방식으로 오픈 소스 소프트웨어를 개발할 수 있는 사업을 당신 주변에 구축했습니다.

저는 당신에게 의지합니다.

연락줄래요? (Call Me Maybe)

제 솔직한 감정을 말했어요. 이젠 당신도 그랬으면 좋겠습니다. 위에서 언급한 포인트들에 대응할 계획이 있나요? 있다면 언제인가요? 저 같은 라이브러리 개발자들에 대해 어떻게 생각하나요? 당신을 잊고 다른 것으로 옮겨가야 할까요? 아니면 같이 우리의 관계 개선을 위해 노력하는 것이 좋을까요?

다음은 뭘까요? 저는 이제 모르겠어요.

image

2022-09-21 리액트가 답신을 보냈어요! 링크에서 확인해보세요

10개의 댓글

comment-user-thumbnail
2022년 10월 4일

좋은 글 감사합니다. ^^ 번역이 엄청 재밌습니다.

답글 달기
comment-user-thumbnail
2022년 10월 4일

대체 이게 뭐여

답글 달기
comment-user-thumbnail
2022년 10월 4일

ㅋㅋㅋ 재밌는 글이네요 :) 예전 기억들이 아련하게 생각나는 글입니다. 저는 그래서 이제 리액트랑 헤어졌지만 아직도 잘나가는 그녀(?)를 볼때마다 내가 잘한게 맞을까? 하면서 괴로운적도 있었지만 이글을 보면서 다시 한번 위안을 얻고 갑니다! 은빈님의 양질의 번역글 잘 읽고 갑니다~~

1개의 답글
comment-user-thumbnail
2022년 10월 6일

리액트 개발자 dan_abramov가 보낸 답장도 재미있네요! 읽다가 흥미가 생겨서 번역해봤습니다.

https://velog.io/@phw3071/번역-리액트가-보낸-리액트-널-사랑하지만-넌-나를-슬프게-해의-답장

답글 달기
comment-user-thumbnail
2022년 10월 6일

백엔드로 일하다가 얼마전에 다시 React 코드를 짤 일이 있었는데 제가 생각하는 React의 문제점이 다 있군요.
그때는 제가 잘 모르니까 다시 생각해봐야지 했었는데... 이 글을 통해서 정말 React에 대해서 다시 생각하게 되네요
재밌는 글, 번역 감사합니다

답글 달기
comment-user-thumbnail
2022년 10월 7일

그러니깐 preact 를 쓰세용 도네도 하고용 ♡ 번역 감사합니다

답글 달기
comment-user-thumbnail
2022년 10월 9일

수상할 정도로 React에 진심이신 분...

답글 달기
comment-user-thumbnail
2022년 10월 12일

좋은 글 감사합니다. 재미있게 읽었어요!!

답글 달기