리액트가 다 해. 나는 그냥 타이핑만 할께

·2024년 3월 6일
0

신기한 개발 세상

목록 보기
1/12
post-thumbnail

흥미진진허다

네이버에서 발행하는 뉴스레터를 읽고 있는데 흥미로운 부분이 있어서 캡쳐해왔다.

이번에 어떤 회사 과제 전형을 진행하다가 성능을 신경써달라는 조건이 있었다. 그래서
useCallback,useMemo, React.memo를 열심히 썼다. 근데 적절한게 쓴거 같은가? 에 대해 물어본다면..? (그래서 그런가~ 면접까진 갔는데 떨어졌다!! 나는 하염없이 눙물이나ㅠㅠ)

사실 프론트 개발자로 3년정도 일을 했지만 아직도 어떻게 쓰는게 좋은건지 명확하게는 모르겠다!!

긍데 이걸 이제 리액트가 알아서 컴파일해서 해준다고?!

뉴스레터에서 링크된 원문이 궁금하다면 여기를 클릭하시오!

위 글의 일부를 번역해서 읽어보면

Memoized React

function App() {
  const [state, setState] = useState()
  function onSubmit() {
    // 제출 로직
  }
  return <form onSubmit={onSubmit}></form>
}

이 함수는 리렌더링될 때마다 자신을 재생성할 것이므로 메모리에서 완전히 새로운 함수가 됩니다. 이것은 일반적으로 함수가 자신을 재생성하는 것이 문제가 되지 않으며, 이 예제에서는 우리에게 어떤 문제도 일으키지 않습니다. 하지만, 이것이 클래스에서 발생하지 않았다는 것을 주목할 가치가 있습니다. 왜냐하면 그것은 render 단계와 별개의 메소드였기 때문입니다.

자바스크립트에서 필요한 것들이 자신을 재생성해야 한다는 일반적인 아이디어는 React에만 국한된 것은 아닙니다.

이제 코드를 조금 리팩토링 해보겠습니다:

function App() {
  const [state, setState] = useState()
  function onSubmit() {
    // 제출 로직
  }
  return <Form onSubmit={onSubmit} />
}

const Form = ({ onSubmit }) => {
  // ...
}

여전히 onSubmit이 매 렌더링마다 새로운 함수가 되는것은 문제가 되지 않습니다. App의 리렌더링이 Form의 리렌더링을 야기할 것입니다. 어떤 이들은 컴포넌트가 그것의 prop이 변경될 때만 리렌더링된다고 말합니다. 하지만 사실이 아닙니다. Form은 App이 리렌더링될 때 prop에 관계없이 리렌더링됩니다. 지금으로서는 onSubmit prop이 변경되는지 여부가 중요하지 않습니다.

이제 App이 리렌더링될 때 Form이 리렌더링되지 않도록 하는 이유가 있다고 가정해 봅시다. Form을 메모이즈하자고 가정해 봅시다:

// 이제 Form은 특정 prop들이 변경될 때만 리렌더링됩니다.
const Form = React.memo(({ onSubmit }) => {
  // ...
});

이제 문제가 발생합니다.

React는 변수가 변경되었는지 알기 위해 엄격한 동등성 체크에 크게 의존합니다. 이것은 고급적인 방법으로 ===와 Object.is()를 사용하여 오래된 것과 새로운 것을 비교한다는 것을 의미합니다. 자바스크립트가 ===를 사용하여 서로 비교할 때 원시값을 값으로 비교합니다. 하지만 JS가 배열, 객체, 함수를 서로 비교할 때 ===의 사용은 그들의 정체성, 즉 그들의 메모리 할당을 비교합니다. 이것이 {} === {}가 자바스크립트에서 false인 이유입니다.

Form = React.memo(fn)을 하는 것은 다음과 같습니다:

안녕하세요 React, 우리는 정말로 prop들이 변경되었을 때에만 Form을 리렌더링하길 원합니다.

이것은 onSubmit이 App이 리렌더링될 때마다 변경되게 됩니다. 이것은 Form이 항상 리렌더링되어 메모이제이션이 우리에게 아무런 도움이 되지 않는다는 것을 의미합니다. 이 시점에서 React에 대한 의미 없는 오버헤드입니다.

이제 우리는 onSubmit이 App이 리렌더링될 때 정체성이 변경되지 않도록 해야 합니다:

function App() {
  const [state, setState] = useState();

  const onSubmit = useCallback(() => {
    // 제출 로직
  }, []);

  return <Form onSubmit={onSubmit} />;
}

우리는 useCallback을 사용하여 함수의 정체성이 변경되지 않도록 합니다. 어떤 면에서 이것은 메모이제이션의 한 형태입니다. 지나치게 단순화해서 말하자면 메모이제이션은 함수의 응답을 "기억하거나" "캐시하는" 것을 의미합니다.

이것은 우리가 말하는 것과 같습니다:

안녕하세요 React, 이 useCallback으로 전달하는 함수의 정체성을 기억하세요. 우리가 리렌더링될 때마다 새로운 함수를 주지만, 그것을 잊고 처음 호출했을 때의 원래 함수의 정체성을 주세요.

onSubmit 함수를 원래는 메모이즈할 필요가 없지만, Form이 메모이즈되었고 onSubmit을 prop으로 받아야 해서 필요성이 생깁니다. React Training에서 이를 "Implementation bleed"이라고 합니다.(원래 코드의 특정 구현 세부 사항이 예상치 못한 방식으로 노출되어, 그로 인해 다른 부분의 코드 작성 방식에 영향을 미치는 상황)

여기에 문제가 끝나지 않습니다. 더 많은 코드를 추가해 봅시다:

function App() {
  const [state, setState] = useState();

  const settings = {};
  const onSubmit = useCallback(() => {
    const x = settings.x;
    // ...
  }, []);

  // ...
}

App의 리렌더링 때마다 settings 객체가 자신을 재생성합니다. 이것 자체로는 문제가 되지 않지만, React를 잘 안다면 linter가 이 경우 useCallback의 의존성 배열에 settings을 넣으라고 요청할 것임을 알고 있을 것입니다:

const settings = {};
const onSubmit = useCallback(() => {
  const x = settings.x;
  // ...
}, [settings]);

이것을 할 때 우리가 말하는 것은 다음과 같습니다:

우리는 onSubmit이 매번 리렌더링할 때마다 안정적으로 유지되기를 원합니다. 하지만 이 의존성 배열에 있는 것들이 변경되면 useCallbackonSubmit을 다시 생성하길 원합니다.

당신은 "왜 onSubmit이 변경되길 원할까?" 라고 스스로에게 물을 수 있습니다.저도 저 질문에 동의합니다. onSubmit은 아마도 변경될 필요가 없을 것입니다. 하지만 React에서 useCallbackuseMemo가 의존성 배열이 변경될 때 그들의 반환값에 대해 새로운 정체성을 다시 메모이제이션하고 생성해야 하는 많은 상황이 있습니다. linter는 이 경우에 우리가 onSubmit이 같은 상태이길 원한다는걸 알지 못 합니다.

linter를 듣고 settings을 의존성 배열에 넣으면 다음과 같은 일이 발생합니다:

App이 리렌더링될 때...

  1. settings은 이전 렌더링에서 ===로 동일하지 않은 새로운 객체가 됩니다.
  2. 의존성 배열은 settings이 값이 변경되지 않았음에도 불구하고 ===에 따라 다르다고 봅니다.
  3. 의존성 배열의 변경은 useCallback이 onSubmit에 대한 새로운 정체성을 반환하게 만듭니다.
  4. onSubmit이 변경되므로 Form이 리렌더링됩니다.

간단히 말해서, Form의 메모이제이션은 쓸모없습니다. App이 리렌더링될 때마다 항상 리렌더링될 것입니다. 그래서 이제 우리는 onSubmit의 메모이제이션을 유지하기 위해 settings을 useMemo로 메모이제이션해야 합니다.

아까 질문을 다시 생각해 봅시다:

왜 나는 onSubmit이 변경되길 원할까요?
그 경우에 linter를 비활성화할 수는 없을까요?

확실히, 이 경우에는 settings을 의존성 배열에서 제외하거나, 제가 아마도 할 것처럼 settings를 메모이제이션하는 것이 좋습니다. 아니면 우리가 애초에 메모이즈된 폼이 필요하지 않았다고 주장할 수도 있습니다. 중요한 것은 React에서 메모이제이션이 종종 Implementation bleed의 연쇄 반응을 야기한다는 것입니다.

메모이제이션에 의존한다는 것

React에서 충분히 작업을 해봤다면 의존성 배열을 다루는 것이 고통스러울 수 있다는 것을 알고 있을 것입니다. linter가 배열에 뭔가를 넣으라고 말하고 당신이 결과를 좋아하지 않을 수 있습니다(예를 들어, 루프처럼). linter에 화를 내기 쉽지만, linter는 옳았습니다. 물론 React가 무한 루프를 "원하는" 것은 아니지만, 이제 뭔가를 메모이제이션해야 한다는 것을 의미했습니다.

의존성 배열은 우리의 모든 코드가 함수형 컴포넌트로 공동 위치하고 시간이 지남에 따라 변수의 변경을 모니터링하고자 하는 사실로 인해 생겼습니다. 때때로 우리는 객체, 배열, 함수를 의존성 배열에 넣어야 하므로, 메모이제이션으로 그것들을 안정화시켜야 합니다. "안정적"이라는 React의 의미를 설명하는 방법은 "당신이 원하지 않는 한 변경되지 않는 변수"입니다.

코드로 이를 시연해 보겠습니다:

function App() {
  const [misc, setMisc] = useState();
  const [darkMode, setDarkMode] = useState(false);
  const options = { darkMode };

  return <User options={options} />;
}

function User({ options }) {
  useEffect(() => {
    // 사용자 정보 가져오기
  }, [options]);

  // ...
}

App에서 misc 상태가 변경되면 그 결과는 options가 변경되고, 따라서 useEffect가 다시 실행될 것입니다. 비록 효과가 misc 상태와 아무 관련이 없더라도 말이죠. 그러니 options 변수를 useMemo로 감싸야 합니다. 그렇게 하면 linter가 당신에게 darkMode를 의존성 배열에 넣으라고 정당하게 요청할 것입니다:

const [darkMode, setDarkMode] = useState(false);
const options = useMemo(() => {
  return { darkMode };
}, [darkMode]);

이렇게 하면 우리는 다음과 같이 말하는 것입니다:

우리는 옵션이 dark mode가 변경될 때까지 안정적이길 원합니다. 그때 새로운 정체성으로 다시 안정화하세요. 하지만 misc 상태가 변경되어도 아무것도 하지 마세요. 왜냐하면 그것은 우리 배열에 없기 때문입니다(우리는 그것에 의존하지 않습니다).

좋아요, 이제 React가 메모이제이션에 의존한다는 것을 이해했기를 바랍니다. 당신은 스스로 작성해야 하며, 올바르게 하지 않으면 버그와 성능 문제가 발생할 것입니다.

자동 메모이제이션을 위한 컴파일

React 팀의 발표는 React가 이전보다 더 많이 컴파일될 것이라는 것입니다. 다른 것들보다 더 많이 컴파일될 것인지는 확실하지 않습니다. 이것이 이 스펙트럼에서 어디에 위치하든 저에게는 중요하지 않습니다. 더 중요한 것은 왜 컴파일하는지입니다. 그 이유는 다른 것들과 다릅니다.

React는 불변성에서 관찰 가능성으로 옮겨가지 않을 것입니다. 여전히 Identity checks와 의존성 배열이 있을 것입니다. React는 몇 년 동안 그랬던 것처럼 여전히 같지만, 자동 메모이제이션을 위해 컴파일될 것이며 수동 메모이제이션의 단점 없이 그대로 유지될 것입니다.

결론..!

여전히 우리가 의존성배열도 쓰고 해야겠지만!! 적어도 메모이제이션을 잘못해서 성능적 단점을 얻게되는 경우는 없지 않을까? 라는게 이 글의 결론인 듯 하다. 리액트 v19에 useForm도 hook으로 들어갈 예정이라던데..이제 나는 타이핑만 잘하면 될 것 같다!

저 글을 읽어보니 나.. useMemo랑 useCallback 좀 잘못쓰고 있던거 같은데 라는 생각도 든다. 그래서 object나 배열 넣을 땐 JSON.stringfy를 해주긴 했는데..몰긋다..너무 어려워잉..

profile
이제는 병아리는 벗어나야하는 프론트개발자

0개의 댓글