마구마구 뒷북치는 React17 변경점

FeRo 페로·2023년 5월 4일
1

지금 리액트는 18버전이다. 정확히는 18.2.0 버전인데, 22년 6월에 나왔다. 나온 지 10개월이 지난 지금 17 변경점을 알아보는 이유는 최근에 18 버전의 변경점을 알아보던 와중에 '그럼 17변경점은 뭐지?'하는 생각이 들어 찾아보니 새로 알게 된 것이 있었다. 많은 것이 바뀐 건 아니지만 그래도 새로운 내용이라서, 시간이 지났어도 정리해 본다.

Gradual Update

여러 레퍼런스에서 17 버전의 업데이트를 '업데이트를 위한 업데이트'라고 표현을 했다. 큰 새로운 기능은 없지만, 미래를 위한 업데이트라고 볼 수 있는 개선된 부분들이 있다. 그 중 하나가 이 점진적 업데이트(Gradual Update)이다. 점진적 업데이트는 두 개 이상의 React가 하나의 어플리케이션에서 함께 구동되는 것을 말한다. 장점은 새로 React가 업데이트 됐을 때 한 번에 모든 걸 업데이트 할 필요 없다는 것이다. 그러나 이것은 여지를 주는 것이지 추천하는 것은 아니다. 대부분 상황에서는 늘 하나의 React 버전을 사용하도록 노력해야 한다. 단일 버전을 사용하는 것이 복잡도를 낮추고 React core를 두 번 다운로드 하는 일을 막을 수 있다.

Normally, we encourage you to use a single version of React across your whole app.
Using a single version of React removes a lot of complexity.
It is also essential to ensure the best experience for your users who don't have to download the code twice. Always prefer using one React if you can.

Event Delegation

사실 이건 이전에도 할 수 있었다. React는 특정 DOM으로부터 Virtual DOM을 만들고 관리하기 때문에 독립적으로 하나의 페이지에서 복수의 React를 사용하거나 중첩된 구조로 사용할 수 있었다. 하지만 이 경우에 기술적인 문제가 있는데, 바로 이벤트 위임과 관련된 문제였다. 17버전 이전은 React Tree 내부의 event.stopPropagation이 다른 React Tree로의 이벤트 위임을 막을 수가 없었다.
그래서 React를 나누어 사용할 때 UI Tree를 추적할 때 예상치 못한 일들이 발생했던 것이다. 원인은 React 17 이전에는 두 개의 React에 있는 이벤트 리스너가 document 레벨에 부착됐기 때문이다. 17버전부터는 이벤트 리스너를 Root에 붙이는 것으로 수정했다.

Import React

원래 jsx를 사용하기 위해서 우리는 매번 React를 Import 해줘야 했다. jsx코드가 React.createElement() 코드로 변환해야 하기 때문이다.
하지만 17버전부터는 React를 따로 Import 하지 않아도 된다. 주요 변경점이라기 보다는 추가된 기능이다. 이렇게 가능한 이유는 새로운 jsx 트랜스폼 방식이 지원됐기 때문이다. 새로운 트랜스폼은 React를 import 하지 않아도 되는 것 외에도, 이전 jsx 트랜스폼에 비해 번들링 시 번들 크기를 약간 개선할 수 있다.

Upgrading to the new transform is completely optional, but it has a few benefits:

  • With the new transform, you can use JSX without importing React.
  • Depending on your setup, its compiled output may slightly improve the bundle size.
  • It will enable future improvements that reduce the number of concepts you need to learn React.

예시코드는 Introducing the New JSX Transform에서 가져왔습니다.

import React from 'react';

function App() {
  return <h1>Hello World</h1>;
}

원래는 위와 같은 코드는 jsx가 내부적으로 javascript 코드로 변환한다.

import React from 'react';

function App() {
  return React.createElement('h1', null, 'Hello World');
}

하지만 이 방식은 완벽하지 않다. React를 import 해야 하는 것과는 별개로 createElement()의 성능 문제가 있다. 이런 문제를 해결한 새로운 jsx 트랜스폼은 다음으로 jsx를 변환한다.

// Inserted by a compiler (don't import it yourself!)
import { jsx as _jsx } from 'react/jsx-runtime';

function App() {
  return _jsx('h1', { children: 'Hello World' });
}

이제 새로운 jsx 트랜스폼은 컴파일 과정에 자동으로 위와 같은 jsx를 변환하는 코드가 추가된다. 새로운 트랜스폼은 'react/jsx-runtime' 모듈에서 자동으로 가져오기 때문에 jsx를 사용할 때 따로 React를 import 하지 않아도 되는 것이다.

생명주기 메소드

리액트 17에 와서는 componentWillMount와 componentWillUpdate, componentWillReceiveProps 라이프 사이클이 deprecated 됐다. 몇 일 전에 우연히 제로초님 블로그의 React의 생명 주기(Life Cycle) 글을 읽다가 이 부분을 알게 되었다. 사실 여기서부터 '그럼 React17 버전에 이것 말고 뭐가 또 바뀌었을까?'하는 궁금함이 생겨서 찾다가 이 글로 정리하게 되었다.

Event Pooling

이벤트 풀링이 없어졌다. 리액트에서는 다양한 브라우저 환경에서도 동일하게 이벤트를 처리하기 위해서 이벤트 래퍼인 SyntheticEvent(합성 이벤트)를 제공한다. 17 이전에서는 이 합성 이벤트 객체를 성능상 이유로 재사용했다. 그래서 사용 이후에는 null로 초기화를 했는데, 그래서 다음과 같은 코드에서는 이벤트에 접근할 수 없었다.

// code from https://ko.legacy.reactjs.org/docs/legacy-event-pooling.html
function handleChange(e) {
  // This won't work because the event object gets reused.
  setTimeout(() => {
    console.log(e.target.value); // Too late! 여기서 null이 출력된다.
  }, 100);
}

그래서 여기서 합성 이벤트 객체 접근을 하기 위해서는 persist 메소드를 사용해야 했다.

function handleChange(e) {
  // Prevents React from resetting its properties:
  e.persist(); // 이걸 사용해야 아래의 콘솔에서 이벤트 객체에 접근할 수 있었다.

  setTimeout(() => {
    console.log(e.target.value); // Works
  }, 100);
}

하지만 17부터는 합성 이벤트 객체를 재사용하지 않는다. 그래서 위의 코드처럼 persist를 사용하지 않고도 개발자가 생각한 대로 이벤트 객체에 접근할 수 있다. 성능 향상을 위해서지만 최신 브라우저에서는 이에 대한 효과가 미미하고 사용자의 혼란을 유발했기 때문에 삭제된 것 같다. 또 이에 대한 근본적인 버그를 해결했다는 이야기도 있다. 물론 persist 메소드는 여전히 남아있지만 호출해도 아무런 동작을 하지 않는다.

useEffect clean up timing

useEffect는 렌더링 이후에 비동기적으로 동작한다. 동기적인 작업이 필요할 때는 useLayoutEffect를 사용하면 된다. 하지만 16버전까지의 useEffect에는 문제가 하나 있었는데 클린업(return부분)은 동기적으로 동작했다. 이게 문제가 되는 이유는 클린업 부분에 무거운 동작을 하는 함수라도 있다면 unmount하기 전에 클린업을 기다렸다가 unmount를 하고 다음 화면을 렌더링 할 수 있다.
17부터는 이런 것을 보완하기 위해서 클린업 함수도 항상 비동기적으로 실행된다. 그리고 모든 useEffect는 클린업 처리가 끝나고 나서 호출된다.

마무리

17버전에 바뀐 게 많이 없다는데, 그럼 다른 버전에서는 얼마나 많은 걸까? 18버전 변경점도 다시 상세히 살펴보아야겠다. 특히나 useEffect 부분은 부트캠프 시 사람들끼리 모여서 직접 로그 찍어보면서 실험해 보기도 했는데 이 부분이 17버전 업데이트 내용이라는 것, 그리고 왜 그렇게 되었는지에 대해서 알아보면서 머리에 깊이 각인되었다. 뒷북이라도 정리를 했음에 다행스러움을 느낀다.

참고자료
https://leo.works/2012130/

https://yamoo9.github.io/react-master/lecture/r-version-17.html#%E1%84%89%E1%85%A2%E1%84%85%E1%85%A9%E1%84%8B%E1%85%AE%E1%86%AB-%E1%84%80%E1%85%A5%E1%86%AB-%E1%84%8B%E1%85%A5%E1%86%B9%E1%84%8B%E1%85%B3%E1%86%B7

https://github.com/reactjs/react-gradual-upgrade-demo/

https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html

https://ko.legacy.reactjs.org/docs/handling-events.html

https://ko.legacy.reactjs.org/docs/events.html#gatsby-focus-wrapper

https://ko.legacy.reactjs.org/docs/legacy-event-pooling.html

https://ko.legacy.reactjs.org/docs/hooks-reference.html#useeffect

https://www.zerocho.com/category/React/post/579b5ec26958781500ed9955

profile
주먹펴고 일어서서 코딩해

0개의 댓글