Home 리액트 v17.0 RC: 새 기능 없음
Post
Cancel

리액트 v17.0 RC: 새 기능 없음

리액트 v17.0 RC: 새 기능 없음

오늘 우리는 리액션 17의 첫 번째 릴리즈 후보를 발표한다. 우리의 기준으로도 긴 시간인 리액트의 이전 주요 릴리즈 이후 2년 6개월 만이다! 이 블로그 게시물에서는 이 주요 릴리스의 역할, 이 릴리스에서 어떤 변화를 기대할 수 있는지, 그리고 이 릴리스에 대해 어떻게 시도해 볼 수 있는지에 대해 설명하겠다.


새 기능 없음

리액트 17 릴리즈는 개발자 기능을 새로 추가하지 않아 이례적이다. 대신 이 릴리스는 React 자체를 쉽게 업그레이드할 수 있도록 하는 데 주로 초점을 맞추고 있다.

우리는 새로운 리액션 기능을 적극적으로 연구하고 있지만, 그것들은 이번 릴리즈에 포함되지 않는다. 리액트 17 릴리즈는 아무도 남기지 않고 롤아웃하는 전략의 핵심 부분이다.

특히 리액션 17은 한 버전의 리액션이 관리하는 트리를 다른 버전의 리액션이 관리하는 트리 안에 더 안전하게 삽입할 수 있는 ‘스텝스톤(돌다리)’ 릴리즈다.


점진적 업그레이드

지난 7년 동안 리액트 업그레이드는 “전부 아니면 전무”이었다. 당신은 이전 버전으로 유지하거나 전체 앱을 새 버전으로 업그레이드했다. 거기에는 중간이 없었다.

지금까지 잘 풀렸지만, 우리는 “전부 아니면 전무” 업그레이드 전략의 한계에 부딪히고 있다. 예를 들어, 레거시 컨텍스트 API를 더 이상 사용하지 않는 일부 API 변경은 자동화된 방식으로 수행할 수 없다. 비록 오늘날 쓰여진 대부분의 앱들이 그것들을 사용하지 않지만, 우리는 여전히 리액트에서 그것들을 지원한다. 우리는 무한정 리액트에서 그들을 지원하는 것과 이전 버전의 리액트에서 몇몇 앱을 남겨두는 것 중 하나를 선택해야 한다. 이 두 가지 방법 모두 훌륭하지 않다.

그래서 우리는 다른 옵션을 제공하기를 원했다.

리액트 17은 점진적인 리액트 업그레이드를 가능하게 한다. 리액트 15에서 16(또는 곧 리액트 16에서 17로 업그레이드)로 업그레이드하면 대개 한 번에 전체 앱을 업그레이드한다. 이것은 많은 앱에서 잘 작동한다. 하지만 만약 코드베이스가 몇 년 전에 쓰여졌고 적극적으로 유지되지 않는다면 그것은 점점 더 어려워질 수 있다. 그리고 페이지에서 리액트의 두 가지 버전을 사용할 수 있지만 리액션 17까지 이것은 취약하고 이벤트와 관련된 문제를 야기했다.

우리는 리액션 17으로 이러한 많은 문제를 해결하고 있다. 이것은 리액트 18과 다음 미래 버전이 나올 때, 여러분은 이제 더 많은 선택권을 갖게 될 것을 의미한다. 첫 번째 옵션은 이전에 했던 것처럼 한 번에 전체 앱을 업그레이드하는 것이다. 그러나 앱을 하나씩 업그레이드할 수도 있다. 예를 들어, 대부분의 앱을 리액션 18으로 마이그레이션하기로 결정할 수 있지만, 리액션 17에 lazy-loaded dialog나 하위 라우트를 유지하도록 결정할 수 있다.

이것은 당신이 점진적인 업그레이드를 해야 한다는 것을 의미하지 않는다. 대부분의 앱에서 한 번에 업그레이드하는 것이 여전히 최고의 솔루션이다. 두 가지 버전의 리액션을 로드하는 것(둘 중 하나가 요청 시 느리게 로드되더라도)은 여전히 이상적이지 않다. 그러나, 적극적으로 유지되지 않는 대형 앱의 경우, 이 옵션을 고려하는 것이 타당할 수 있으며, 리액션 17은 이러한 앱들이 뒤처지지 않도록 한다.

점진적인 업데이트를 가능하게 하기 위해, 우리는 리액트 이벤트 시스템을 몇 가지 변경해야 했다. 리액션 17은 이러한 변화들이 잠재적으로 깨질 수 있기 때문에 주요 발표물이다. 실제로 10만개 이상의 구성 요소 중 20개 미만의 구성 요소만 변경하면 되므로 우리는 대부분의 앱이 큰 문제 없이 리액트 17로 업그레이드할 수 있을 것으로 예상한다. 만약 문제가 생기면 우리에게 말해주길 바란다.


점진적 업그레이드의 데모

우리는 필요한 경우 이전 버전의 리액트을 lazy-load하는 방법을 보여주는 예제 repository를 준비했다. 이 데모에서는 Create React App을 사용하지만 다른 도구와 유사한 접근법을 따를 수 있어야 한다. 우리는 다른 도구를 pull requests으로 사용하는 데모를 환영한다.

Note

우리는 리액트 17 이후로 다른 변경을 연기했다. 이 릴리스의 목표는 점진적인 업그레이드를 가능하게 하는 것이다. 리액트 17로 업그레이드하는 것이 너무 어려웠다면, 그것은 그것의 목적을 좌절시킬 것이다.


이벤트 위임에 대한 변경 사항

기술적으로, 다른 버전의 리액트로 개발된 앱을 중첩하는 것은 항상 가능했다. 그러나 리액트 이벤트 시스템이 작동하는 방식 때문에 다소 연약했다.

리액트 컴포넌트에서, 우리는 일반적으로 이벤트 핸들러를 인라인으로 작성한다.

1
<button onClick={handleClick}>

이 코드와 같은 바닐라 DOM은 다음과 같다.

1
myButton.addEventListener("click", handleClick);

그러나 대부분의 이벤트의 경우 React는 실제로 해당 이벤트를 선언하는 DOM 노드에 연결하지 않는다. 대신 React는 이벤트 유형당 하나의 핸들러를 document 노드에 직접 부착한다. 이를 이벤트 위임이라고 한다. 대형 애플리케이션 트리에 대한 성능 이점 외에도, replaying 이벤트와 같은 새로운 기능을 더 쉽게 추가할 수 있도록 한다.

리액트는 첫 출시 이후 자동으로 이벤트 위임을 진행하고 있다. DOM 이벤트가 document에 실행될 때, 리액트는 어떤 컴포넌트를 호출할지 알아낸 다음 리액트 이벤트가 컴포넌트를 통해 “버블들” 위로 이동한다. 그러나 그 이면에는 이미 기본 이벤트가 document 레벨까지 버블 업 했고, 리액트는 이벤트 핸들러를 설치한다.

그러나 이것은 점진적인 업그레이드의 문제다.

만약 당신의 페이지에 리액트 버전이 여러 개 있는 경우, 그들을 모두 맨 위에 이벤트 핸들러들로 등록하자. 이렇게 하면 e.stopPropagation()이 깨진다. 중첩된 트리가 이벤트의 전파를 중지한 경우에도 외부 트리는 이벤트를 계속 수신할 수 있다. 이것은 리액트의 다른 버전을 중첩하는 것을 어렵게 만들었다. 이러한 우려는 가상적인 것이 아니다. - 예를 들어, 아톰 에디터는 4년 전에 이 문제에 부딪혔다.

이것이 리액트가 후드 아래의 DOM에 이벤트를 부착하는 방법을 바꾸는 이유다.

리액트 17에서, 리액트는 더 이상 document 수준에서 이벤트 핸들러를 부착하지 않는다. 대신 리액트 트리가 렌더링되는 루트 DOM 컨테이너에 부착한다.

1
2
const rootNode = document.getElementById("root");
ReactDOM.render(<App />, rootNode);

리액트 16 및 이전 버전에서 리액트는 대부분의 이벤트에 대해 document.addEventListener()를 수행한다. 리액트 17은 후드 아래에 rootNode.addEventListener()를 대신 호출한다.

이러한 변화 덕분에, 이제 한 버전에 의해 관리되는 리액트 트리를 다른 리액트 버전에 의해 관리되는 트리 안에 넣는 것이 더 안전해졌다. 이를 위해서는 두 버전이 모두 17 이상이어야 하므로 리액트 17로 업그레이드하는 것이 중요하다는 점에 유의하자. 어떻게 보면 리액트 17은 다음 단계적 업그레이드를 가능하게 하는 “돌다리(stepping stone)” 릴리즈다.

이러한 변화는 또한 다른 기술로 구축된 앱에 리액트를 더 쉽게 내장할 수 있게 한다. 예를 들어, 앱의 외부 “shell”이 jQuery로 작성되었지만 그 안의 새로운 코드가 리액트로 작성되면, 예상하듯이 이제 리액트 코드 내부의 e.stopPropagation()이 jQuery 코드에 도달하는 것을 막을 수 있다. 이것은 다른 방향에서도 작용한다. 더 이상 리액트를 좋아하지 않고 (예를 들어 jQuery에서) 앱을 다시 쓰려면 이벤트 전파를 중단하지 않고 외부 shell을 리액트에서 jQuery로 변환하기 시작할 수 있다.

우리는 우리의 이슈 트래커에서 리액트에서 리액트가 아닌 코드로 옮기는 것과 관련된 수년간 보고된 수많은 문제들이 새로운 행동에 의해 해결되었다는 것을 확인했다.

Note

당신은 이것이 루트 컨테이너 밖의 포탈(설명링크)을 파괴하는 것이 아닌지 궁금할 것이다. 답은 리액트는 포털 컨테이너의 이벤트도 청취하기 때문에 이 문제는 문제가 되지 않는다는 것이다.


잠재적인 문제 해결

획기적인 변화든지 간에, 그것은 약간의 코드를 조정해야 할 것 같다. 페이스북에서는 이러한 변화에 대처하기 위해 총 10개 정도의 모듈(수천 개 중)을 조정해야 했다.

예를 들어 document.addEventListener(…)와 함께 수동 DOM 리스너를 추가하면 모든 React 이벤트를 캐치할 것으로 예상할 수 있다. 리액트 16 및 이전 버전에서는 리액트 이벤트 핸들러에서 e.stopPropagation()을 호출하더라도, 기본 이벤트가 이미 document 수준에 있기 때문에 사용자 정의 document 리스너가 수신할 수 있다. 리액트 17을 사용하면 전파가 중지되므로(요청된 대로!) document 핸들러가 다음을 실행하지 않는다.

1
2
3
4
document.addEventListener("click", function () {
  // This custom handler will no longer receive clicks
  // from React components that called e.stopPropagation()
});

캡처 단계를 사용하도록 리스너를 변환하여 이와 같은 코드를 수정할 수 있다. 이렇게 하려면 document.addEventListener: 에게 { capture: true } 을 세번째 인자로 전달 하면 된다.

1
2
3
4
5
6
7
8
document.addEventListener(
  "click",
  function () {
    // Now this event handler uses the capture phase,
    // so it receives *all* click events below!
  },
  { capture: true }
);

이 전략이 전반적으로 어떻게 더 탄력적인지 주목하자. 예를 들어, 이 전략은 e.stopPropagation()가 리액트 이벤트 핸들러 외부에서 호출될 때 발생하는 기존 버그를 해결할 수 있을 것이다. 즉, 리액트 17의 이벤트 전파는 일반 DOM에 더 가깝게 작용한다.


기타 획기적인 변화들

우리는 리액트 17의 획기적인 변화들을 최소한으로 유지했다. 예를 들어, 이전 버전에서 더 이상 사용되지 않은 메소드들은 제거되지 않는다. 그러나, 그것은 우리의 경험에서 상대적으로 안전했던 몇 가지 다른 획기적인 변화들을 포함하고 있다. 모두 합쳐서 10만 개 이상의 컴포넌트들 중 20개 이하로 조정해야 했다.

브라우저에 맞게 조정

우리는 이벤트 시스템과 관련하여 몇 가지 작은 변경사항을 적용했다.

  • onScroll 이벤트는 더 이상 버블이 없어서 일반적인 혼란을 방지한다.
  • React onFocus 및 onBlur 이벤트는 후드 아래에서 네이티브 focusin 및 focusout 이벤트를 사용하는 것으로 전환되었으며, 이 이벤트는 리액트의 기존 동작과 보다 밀접하게 일치하며 때로는 추가 정보를 제공한다.
  • 캡처 단계 이벤트(예: onClickCapture)는 이제 실제 브라우저 캡처 단계 리스너를 사용한다.

이러한 변경사항은 리액트를 브라우저 동작에 더 가깝게 대응하고 상호운용성을 개선한다.

Note

리액트 17은 onFocus 이벤트의 후드 아래에서 focus 에서 focusin으로 전환되었지만, 이것이 버블링 행동에 영향을 미치지 않았다는 점에 유의한다. 리액트에서, onFocus 이벤트는 항상 버블이 발생했고, 일반적으로 더 유용한 디폴트이기 때문에 리액트 17에서 계속 그렇게 한다. 다양한 특정 사용 사례에 추가할 수 있는 다양한 검사는 이 샌드박스를 참조하자.

이벤트 풀링 없음

리액트 17은 리액트에서 “이벤트 풀링” 최적화를 제거한다. 그것은 현대 브라우저의 성능을 향상시키지 못하고 경험 많은 리액트 사용자들조차 혼란스럽게 한다.

1
2
3
4
5
6
7
function handleChange(e) {
  setData((data) => ({
    ...data,
    // This crashes in React 16 and earlier:
    text: e.target.value,
  }));
}

리액트는 이전 오래된 브라우저의 성능을 위해 다른 이벤트 사이의 이벤트 개체를 재사용하고, 모든 이벤트 필드를 null로 설정했기 때문이다. 리액트 16 및 이전 버전에서는 e.persist()를 호출하여 이벤트를 적절히 사용하거나, 필요한 속성을 일찍 읽어야 한다.

리액트 17에서 이 코드는 예상대로 작동한다. 이전 이벤트 풀링 최적화가 완전히 제거되었으므로 필요할 때마다 이벤트 필드를 읽을 수 있다.

이것은 행동 변화로, 그래서 우리는 그것을 깨는 것으로 표시하고 있지만, 실제로 우리는 페이스북에서 그것이 깨지는 것을 본 적이 없다. (버그를 몇 개 고친 건지도 몰라!) e.persist()는 리액트 이벤트 오브젝트에서 여전히 사용할 수 있지만, 지금은 아무 것도 하지 않는다는 점에 유의하자.

이펙트 클린업 타이밍

우리는 useEffect cleaning 기능의 타이밍을 보다 일관성 있게 만들고 있다.

1
2
3
4
5
6
useEffect(() => {
  // This is the effect itself.
  return () => {
    // This is its cleanup.
  };
});

대부분의 이펙트들은 화면 업데이트를 지연시킬 필요가 없으므로, 리액트는 업데이트가 화면에 반영된 직후 비동기식으로 실행된다. (예: 툴팁을 측정하고 배치하는 등 페인트를 차단하기 위해 효과가 필요한 드문 경우, useLayoutEffect를 선호)

그러나, 이펙트 클린업 기능이 존재하는 경우, 리액트 16에서 동기적으로 실행하는데 사용된다. 우리는 componentWillUnmount가 클래스에서 동기화된 것과 유사하게, 이것은 큰 화면 전환(예: 탭 전환)을 느리게 하기 때문에 더 큰 앱에 이상 적합하지 않다는 것을 발견했다.

리액트 17에서 이펙트 클린업 기능도 비동기적으로 실행된다. 예를 들어 컴포넌트가 마운트 해제된 경우 화면이 업데이트된 후 정리가 실행된다.

이것은 이펙트 그 자체가 어떻게 더 가까이 가는지 반영한다. 드물지만 동기식 실행에 의존할 수 있는 경우에는 useLayoutEffect로 전환할 수 있다.

Note

당신은 마운트되지 않은 컴포넌트의 setState에 대한 경고를 수정할 수 없게 되었는지 궁금할 수 있다. 걱정하지 말자 — 리액트는 이 케이스에 대해 구체적으로 확인하고 마운트 해제와 정리 사이의 짧은 간격에서 setState 경고를 발생시키지 않는다. 따라서 요청이나 인터벌을 취소하는 코드는 거의 항상 동일하게 유지될 수 있다.

또한 리액트 17은 트리에서의 위치에 따라 효과와 동일한 순서로 클린업 기능을 실행한다. 이전에, 이 순서는 때때로 달랐다.

잠재적인 이슈들

재사용 가능한 라이브러리들이 좀 더 철저하게 테스트해야 할 필요가 있을 수도 있지만, 우리는 단지 몇 가지 컴포넌트들이 이러한 변화로 인해 깨지는 것을 보았다. 문제가 있는 코드의 한 예는 다음과 같다:

1
2
3
4
5
6
useEffect(() => {
  someRef.current.someSetupMethod();
  return () => {
    someRef.current.someCleanupMethod();
  };
});

문제는 someRef.current는 변형이 가능하기 때문에 클린업 함수가 실행될 무렵에는 null로 설정되었을 수 있다는 점이다. 해결책은 이펙트 내부의 모든 변형가능한 값을 캡처하는 것이다.

1
2
3
4
5
6
7
useEffect(() => {
  const instance = someRef.current;
  instance.someSetupMethod();
  return () => {
    instance.someCleanupMethod();
  };
});

우리의 eslint-plugin-react-hooks/expower-depends lint rule (사용하도록 해!) 은 항상 이것에 대해 경고해 왔기 때문에 우리는 이것이 흔한 문제가 될 것이라고 예상하지 않는다.

Undefined 반환에 대한 일관된 오류들

리액트 16 및 이전 버전에서 undefined 반환은 항상 오류였다.

1
2
3
function Button() {
  return; // Error: Nothing was returned from render
}

이는 본의 아니게 undefined 상태로 리턴하기 쉽기 때문이다.

1
2
3
4
5
function Button() {
  // We forgot to write return, so this component returns undefined.
  // React surfaces this as an error instead of ignoring it.
  <button />;
}

기존에는 리액트가 클래스 및 함수 컴포넌트들에 대해서만 이 작업을 수행했지만 forwardRef 및 memo 컴포넌트들의 반환 값은 확인하지 않았다. 이것은 코딩 실수에 의한 것이었다.

리액트 17에서 forwardRef 및 memo 컴포넌트들에 대한 동작은 일반 함수 및 클래스 컴포넌트와 일치한다. 그들로부터 undefined 반환은 오류다.

1
2
3
4
5
6
7
8
9
10
11
let Button = forwardRef(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

let Button = memo(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

의도적으로 아무것도 렌더링하지 않으려는 경우 대신 null을 반환하자.

네이티브 컴포넌트 스택들

브라우저에서 오류를 발생시키면 브라우저에서 자바스크립트 함수 이름과 해당 위치가 포함된 스택 추적을 제공한다. 그러나 리액트 트리 계층이 그만큼 중요할 수 있기 때문에 자바스크립트 스택은 문제를 진단하기에 충분하지 않은 경우가 많다. 버튼이 오류를 발생시켰다는 것만이 아니라, 리액트 트리에서 버튼이 어디에 있는지 알고 싶을 것이다.

이 문제를 해결하기 위해 리액트 16은 오류가 있을 때 “컴포넌트 스택들”을 프린트하기 시작했다. 하지만 그들은 원래 자바스크립트 스택에 비해 열세였다. 특히 리액트는 소스 코드에서 함수가 선언된 위치를 몰랐기 때문에 콘솔에서 클릭할 수 없었다. 게다가, 그것들은 프로덕션에서 대부분 쓸모없었다. 소스맵으로 원래 함수 이름에 자동으로 복원할 수 있는 일반 축소화된 JavaScript 스택과는 달리, 리액트 컴포넌트 스택을 사용하면 프로덕션 스택과 번들 크기 중에서 선택해야 했다.

리액트 17에서 컴포넌트 스택은 일반 네이티브 자바스크립트 스택에서 서로 결합하는 다른 메커니즘을 사용하여 생성된다. 이를 통해 프로덕션 환경에서 완전한 기호화된 리액트 컴포넌트 스택 추적을 얻을 수 있다.

리액트가 이것을 구현하는 방법은 다소 비정상적이다. 현재 브라우저는 함수의 스택 프레임(소스 파일과 위치)을 얻을 수 있는 방법을 제공하지 않는다. 그래서 리액트가 오류를 잡을 때, 그것은 가능한 경우 위의 각 컴포넌트 내부에서 임시 에러를 던져(그리고 잡아냄) 그것의 컴포넌트 스택을 재구성할 것이다. 이는 충돌 시 작은 성능 저하를 야기하지만 컴포넌트 타입당 한 번만 발생한다.

궁금하다면, 이 PR에서 더 자세한 내용을 읽을 수 있지만, 대부분의 경우 이 정확한 메커니즘이 코드에 영향을 미치지 않아야 한다. 새로운 기능은 컴포넌트 스택이 이제 클릭 가능하며(원래 브라우저 스택 프레임에 의존하기 때문에), 일반 자바스크립트 오류처럼 프로덕션에서 디코딩할 수 있다는 것이다.

획기적인 변화를 구성하기 위해 리액트는 오류가 포착된 후 스택에서 리액트 함수들 중 일부와 리액트 클래스 생성자의 일부를 재실행한다. 렌더 함수들과 클래스 생성자는 (서버 렌더링에도 중요한) 부작용(side effects)을 가져서는 안 되기 때문에, 이것은 어떠한 실질적인 문제를 제기해서는 안 된다.

Private Exports 제거

마지막으로 마지막으로 주목할 만한 획기적인 변화는 이전에 다른 프로젝트에 노출되었던 리액트 internal들을 제거했다는 것이다. 특히 React Native for Web은 이벤트 시스템의 일부 internal들에 의존했지만, 그 의존성은 취약해서 깨지곤 했다.

리액션 17에서는 이러한 private export가 제거되었다. 우리가 알고 있는 한, React Native for Web은 그것들을 사용하는 유일한 프로젝트였고, 그들은 이미 그러한 private exports에 의존하지 않는 다른 접근법으로의 마이그레이션을 완료했다.

이것은 이전 버전의 React Native for Web은 리액트 17과 호환되지 않지만, 새로운 버전은 이 버전과 함께 작동한다는 것을 의미한다. 실제로, React Native for Web는 내부 리액트 변경에 적응하기 위해 새로운 버전을 출시해야 했기 때문에 이 변경 사항은 크게 변경되지 않는다.

또한, 우리는 ReactTestUtils.SimulateNative 헬퍼 메소드들을 제거했다. 그들은 문서화된 적이 없었고, 그들의 이름이 암시하는 것을 제대로 하지 않았으며, 우리가 이벤트 시스템에 대해 취한 변화들과 함께 작용하지 않았다. 만약 테스트에서 기본 브라우저 이벤트를 편리하게 실행하려면 대신 리액트 테스팅 라이브러리를 확인하자.


설치하기

References

This post is licensed under CC BY 4.0 by the author.

VSCode에서 Kotlin 사용하기

[Spring] 1.15.5. Deploying a Spring ApplicationContext as a Java EE RAR File