sbyeol3/articles

[번역] 리액트 hooks에 대해 이해하기

Opened this issue · 0 comments

원문 : Making Sense of React Hooks을 읽고 번역한 글입니다.

이번주에 Sopie Alpert와 저는 리액트 컨퍼런스에서 "Hooks" 세션에서 발표했고, Ryan Florence의 심층적인 설명을 들었습니다.

저는 Hooks proposal로 해결하고자 하는 문제들을 보기 위해 이 오프닝 키노트를 보시는 것을 강력하게 추천합니다. 그러나 1시간은 꽤 큰 투자이기 때문에 아래에서 몇 가지 Hooks에 대한 생각들을 공유하고자 합니다.

주목 : Hooks는 리액트에서 실험적인 제안입니다. 지금 당장 배울 필요는 없습니다. 또한 이 포스팅은 저의 개인적 견해를 담고 있으며 리액트 팀의 입장을 모두 반영하는 것은 아닙니다.

왜 Hooks인가요?

컴포넌트와 top-down 방식의 데이터 흐름이 큰 UI를 작고 독립적이며 재사용가능한 단위로 나눌 수 있게 한다는 것을 알고 있습니다. 그러나, 우리는 로직이 상태에 연관되어 있거나 함수나 다른 컴포넌트로 분리할 수 없기 때문에 복잡한 컴포넌트를 쪼갤 수 없는 경우가 많습니다. 때때로 사람들은 React에서 "관심사 분리"를 하지 못한다고 하죠.

이런 경우들은 아주 흔하고 애니메이션이나 폼 핸들링, 외부 데이터 소스와 연결되는 경우 등 우리가 컴포넌트로 하고 싶은 많은 것들을 포함합니다. 이러한 경우를 컴포넌트로만 해결하려고 하면 다음과 같은 결과가 생겨납니다.

  • 리팩토링과 테스트하기 어려운 거대한 컴포넌트
  • 다른 컴포넌트들과 라이프사이클 메소드 사이의 중복되는 로직
  • props 렌더나 고차 컴포넌트와 같은 복잡한 패턴

이런 문제들을 해결하기 위한 가장 좋은 방법을 Hooks라고 생각했습니다. Hooks는 컴포넌트 내부의 로직을 재사용 가능하며 분리된 단위로 조직하는 것이 가능합니다. Hooks은 리액트 철학(명시적인 데이터 흐름과 구성)을 컴포넌트 _사이_가 아닌 컴포넌트 내부에 반영합니다. 제가 Hooks가 React 컴포넌트 모델과 자연스럽게 잘 부합한다고 느낀 이유입니다.

props의 렌더나 고차 컴포넌트와 같은 패턴과 다르게 Hooks는 여러분의 컴포넌트 트리에서 불필요한 중첩을 만들지 않습니다. mixin의 단점으로부터 고통받지 않아도 되죠.

(제가 그랬던 것처럼) 여러분이 처음에는 본능적으로 거부하고자 한다 해도, 일단은 시도해보고 다뤄보기를 권합니다. 아마 좋아하실 겁니다.

Hooks이 리액트를 어렵게 만드나요?

Hooks에 대해 자세히 알아보기 전에는 Hooks을 사용하면 더 많은 컨셉을 더해야 한다고 우려하실 수도 있습니다. 합리적인 비판입니다. Hooks을 배우는데 단기적으로는 인지적 비용이 들기는 하겠지만, 최종 결과로는 그 반대가 될 것입니다.

리액트 커뮤니티가 Hooks를 잘 수용한다면, Hooks은 리액트 어플리케이션을 작성하면서 필요한 개념의 수를 줄일 것입니다. Hooks은 함수, 클래스, 고차 컴포넌트, props 렌더 사이에 끊임없이 스위칭할 필요 없이 함수만 사용하면 되게끔 합니다.

구현 사이즈의 관점에서 Hooks은 ~1.5kB만 증가시킬 뿐입니다. 클래스를 작성하여 코드를 작성하는 것보다 Hooks를 채택하여 코드를 작성할 때 코드의 양을 줄일 수 있으므로 Hooks는 결국 여러분의 번들 사이즈를 줄일 수 있습니다. 아래 예시는 다소 극단적이지만 Hooks를 채택해야 하는 이유를 효과적으로 보여줍니다.

원문에서 예시가 사라짐

Hooks를 채택하여 새로운 컴포넌트를 Hooks 기반으로 작성해도 현재 코드에 영향을 주지 않습니다. 사실은 우리가 Hooks를 권장하는 이유는 코드의 재작성이 필요없기 때문입니다. 중요한 코드에서 Hooks로 작성하고 기다리는 것이 좋습니다. React 16.7 알파버전으로 실험해보시고 피드백이 있거나 버그가 있으면 제보해주신다면 감사하겠습니다.

Hooks가 정확히 무엇인가요?

Hooks를 이해하기 위해서 한 걸음 돌아가 코드의 재사용에 대해 생각해보아야 합니다.

오늘날 리액트 앱에서 로직을 재사용하는 방법은 여러 가지가 있습니다. 간단한 함수들을 작성해서 무언가를 연산할 때 함수들을 호출할 수 있습니다. 또한 컴포넌트를 작성할 수도 있죠. 컴포넌트는 더 강력하지만 UI를 렌더링해야 합니다. 이는 시각적이지 않은 로직을 공유할 때 불편합니다. 그래서 render props나 고차 컴포넌트와 같은 복잡한 패턴이 만들어진 것입니다. 많은 코드를 재사용하는 대신 하나의 공통적인 재사용 방법이 있다면 더 간단하지 않을까요?

함수는 코드 재사용에 있어 완벽한 메커니즘으로 보입니다. 함수 사이에 로직을 이동시키는 것은 적은 노력이 듭니다. 그러나 함수 내부에서 React의 로컬 state를 사용할 수 없습니다. 코드를 재구성하거나 옵저버블과 같은 추상화를 도입하지 않는다면 클래스 컴포넌트로부터 "윈도우 사이즈를 보고 상태를 업데이트"하거나 "시간이 지나면서 값 변경하기"와 같은 동작을 꺼내올 수 없습니다. 두 접근법은 리액트의 단순한 성질을 해치게 되죠.

Hooks는 그 문제를 정확하게 해결합니다. Hooks는 함수로부터 리액트 기능을 사용하게 합니다. 리액트는 상태, 라이프사이클, 컨텍스트와 같은 "구성요소"를 노출하는 몇 가지 빌트인 Hooks를 제공합니다.

Hooks는 일반적인 자바스크립트 함수이므로 여러분은 리액트에서 제공하는 빌트인 Hooks와 여러분의 "커스텀 훅"을 결합할 수 있습니다. 이는 복잡한 문제를 한 줄로 해결하고 여러분의 어플리케이션이나 리액트 커뮤니티에 공유할 수 있습니다.

커스텀 훅은 기술적으로는 리액트의 미래가 아닙니다. 여러분의 훅을 작성할 수 있는 것은 Hooks이 설계된 것을 자연스럽게 따라간 결과물입니다.

코드를 보여주세요!

컴포넌트를 현재 윈도우 width에 구독한다고 가정해봅시다. (좁은 뷰포트에 다른 컨텐츠를 보여준다고 가정)

이런 코드를 작성하는 방법은 여러 가지가 있습니다. 클래스를 작성하거나 라이프사이클 메소드를 사용하거나 또는 컴포넌트 사이에 재사용을 하고 싶다면 render prop, 고차 컴포넌트도 방법에 포함되죠. 하지만 이 방법보다 더 좋은 방법은 없다고 생각합니다.

function MyResoponsiveComponent() {
    const width = useWindowWidth(); // custom hook
    return (
        <p>window width is {width}</p>
    )
}

이 코드를 읽으신다면 무엇을 하고자 하는지 바로 알 수 있습니다. 컴포넌트 내에서 _window width_를 사용하고, 리액트는 해당 값이 변경될 때마다 컴포넌트를 다시 렌더링합니다. 컴포넌트에 상태나 사이드 이펙트가 있더라도 컴포넌트를 진정한 선언적으로 만드는 것이 Hooks의 목표입니다.

이 커스텀 훅을 구현하는 방법을 살펴봅시다. 현재 window width를 유지하도록 React 로컬 상태 를 사용하고, window 사이즈가 변경되면 상태 값을 설정하도록 사이드 이펙트사용합니다.

import { useState, useEffect } from 'react';

function useWindowWidth() {
    const [width, setWidth] = useState(window.innerWidth);
    
    useEffect(() => {
        const handleResize = () => setWidth(window.innerWidth);
        window.addEventListener('resize', handleResize);
        return  () => {
            window.removeEventListener('resize', handleResize);
        }
    });
    
    return wudth;
}

위에서 봤듯이 useStateuseEffect 와 같은 빌트인 리액트 Hooks가 기본 구성 요소로 제공됩니다. 컴포넌트로부터 직접적으로 사용할 수 있고 useWindowWidth 같은 커스텀 훅과 같이 사용할 수 있습니다. 커스텀 훅을 사용하는 것이 더 관용적으로 느껴집니다.

빌트인 Hooks에 대해 더 알고 싶다면 이 개요를 참고하세요.

Hooks은 완전히 캡슐화 되어 있어, 여러분이 Hook을 호출할 때마다 Hook은 실행하는 컴포넌트 내에 독립적인 로컬 상태를 가집니다. 우리 예시에서는 중요하지는 않지만 이 특징이 Hooks를 강력하게 만들어 줍니다. 상태 를 공유하는 방식이 아니라 상태 저장 논리 를 공유하는 방식입니다. top-down 데이터 흐름을 망치지 않습니다.

각 Hook은 로컬 상태와 사이드 이펙트를 가집니다. 일반적으로 함수들 사이에서 하듯이 여러 Hooks 사이에 데이터를 전달할 수 있습니다. Hooks는 자바스크립트 함수이기 때문에 인수를 받아 값을 반환합니다.

Hooks 사이에 데이터를 전달할 수 있는 것은 애니메이션, 데이터 구독, 폼 관리, 그리고 다른 상태 추상화를 표현하는 데 잘 들어 맞습니다. render props나 고차 컴포넌트와 다르게 Hooks는 렌더 트리에 "false hierarchy"를 만들지 않습니다. 컴포넌트에 부착된 평평한 "메모리 셀" 리스트에 더 가깝습니다. 추가적인 레이어를 만들지 않습니다.

그러면 클래스에서는 어떻게 하나요?

우리의 의견으로는 커스텀 훅이 Hooks 제안에서 가장 매력적인 요소입니다. 그러나 Hooks를 사용하기 위해서 React는 상태와 사이드 이펙트를 선언할 방법으로 함수를 제공해야 합니다. 그리고 useStateuseEffect 가 실제로 하는 방식이죠. 이 문서에서 이 Hooks에 대해 더 배울 수 있습니다.

빌트인 Hooks가 커스텀 훅을 생성할 때만 유용한 것은 아닙니다. Hooks는 또한 일반적으로 컴포넌트를 정의하는 데 있어 사용 가능하고 상태와 같이 필요한 기능들을 제공합니다. 미래에 리액트 컴포넌트를 정의하는 데 있어 Hooks가 1차적인 방법이 되기를 원하는 이유입니다.

클래스를 없앨 계획은 없습니다. 여러분과 마찬가지로 페이스북에서도 무수히 많은 클래스 컴포넌트를 사용하고 있고 이를 재작성하고 싶지 않습니다. 그러나 리액트 커뮤니티에서 Hooks를 받아들인다면, 컴포넌트를 작성하는 데 두 가지 방법을 둘 필요가 없습니다. Hooks는 클래스의 모든 상황을 다 커버할 수 있습니다. 오히려 추출, 테스팅, 코드 재사용 면에 있어 더 유연함을 보여줍니다. 리액트의 미래에서 Hooks를 우리의 비전으로 제시하는 이유입니다.

Hooks는 마법이 아닌가요?

Hooks의 규칙에 놀라셨을 수도 있습니다.

Hooks는 탑 레벨에서 호출하는 것은 일반적이지 않지만, 가능한 경우에도 상태를 정의하고 싶지 않을 수 있습니다. 예를 들어 클래스에서 상태를 조건부로 정의할 수 없고 리액트 사용자에게도 이런 불만을 들은 적은 없습니다.

이러한 설계는 추가적인 구문이나 여러 함정을 만들지 않고 커스텀 훅을 활성화하는 데 아주 중요합니다. 처음에는 생소할 수 있지만 결국은 Hooks만의 기능이 가지는 가치로 트레이드 오프 할 수 있다고 생각합니다. 동의하지 않으신다면 일단 연습으로 갖고 놀아보면서 마음이 바뀌시는지 확인해보시길 바랍니다.

저희는 엔지니어들이 Hooks의 규칙에 혼란스러워 하지 않는지 확인해보고자 한달동안 실제 개발할 때 사용하고 있습니다. 실제로 개발자들이 몇 시간 안에 익숙해진다는 것을 발견했습니다. 개인적으로 저도 처음에 규칙들이 잘못되었다고 느꼈기는 하지만 금방 극복했습니다. 이 경험은 리액트를 처음 경험했을 때와 비슷했습니다. (바로 리액트를 좋아하셨나요? 저는 두 번 해보고 좋아했습니다.)

Hooks의 구현에 있어 "마법"은 없습니다. Jamie가 지적한 바와 같이 이것은 아래와 비슷해 보입니다.