August 17th 2019
Making Sense of React Hooks 번역글입니다.
이번 주 Sophie Alpert와 저는 React Conf에서 "Hooks"에 대해 발표를 했고, 뒤이어 Ryan Florence의 세세한 설명이 있었습니다.
저는 위 발표 영상을 꼭 보시는 것을 추천합니다. 영상에서 Hooks를 이용해 저희가 맞닥뜨린 문제를 어떻게 해결하는지 설명하고 있습니다. 하지만, 1시간가량의 시간은 짧지 않기 때문에 "Hook"에 관한 몇 가지 생각을 아래와 같이 공유하고자 합니다.
Hooks는 아직은 실험적인 단계이기 때문에, 당장 꼭 알아야 하는 것은 아닙니다. 이 포스팅에서는 저의 개인적인 주관이 담겨있고, React team의 의견을 대신하는 것은 아닙니다.
우리는 UI를 세분화하고, 독립적이고, 재사용성을 높게 하기 위해서 컴포넌트와 top-down 방식의 데이터 흐름을 사용합니다. 히지만, 우리는 종종 복잡한 컴포넌트를 세분화할 수 없습니다. 로직이 컴포넌트 내부 state와 연결되어 있고, 함수나 다른 컴포넌트로 옮길 수 없기 때문입니다. 때때로, 사람들이 React에서 관심의 분리(separate concerns)를 적용할 수 없다고 말하는 이유이기도 합니다.
위의 상황은 매우 일반적이며, 애니메이션, 폼 핸들링, 외부 데이터 요청 등의 여러 작업을 하며 겪을 수 있습니다. 이러한 문제를 컴포넌트로만 해결하고자 하면, 보통 아래와 같은 단계를 겪게 됩니다.
"Hooks"는 위 문제를 해결하기 위한 최적의 방법이라고 생각합니다. "Hooks"는 컴포넌트 내부 로직을 재사용할 수 있고 별도의 독립된 단위로 구분할 수 있게 해 줍니다.
Hooks는 React의 철학(명시적 데이터 흐름과 구성)을 컴포넌트 간이 아닌 컴포넌트 내부에서 적용합니다. 이것이 제가 Hooks가 React 컴포넌트 모델에서 잘 맞는 이유라고 생각합니다.
render props 또는 higher order component와 같은 패턴과는 다르게, Hooks는 컴포넌트 트리에 불필요한 네스팅을 만들지 않습니다. Mixin의 결점에서 오는 어려움을 겪을 필요도 없습니다.
저는 시험 삼아 또는 호기심에서라도 이것을 한 번 사용해 보는 것을 추천합니다.
Hooks를 살펴보기 전, 여러분은 리액트에 점점 더 많은 개념이 도입되는 것을 걱정할 수도 있습니다. 저는 이러한 걱정은 당연하다고 생각합니다. 하지만 Hooks를 배우기 위해 어느 정도 시간과 인지적인 면에서 비용이 들지만, 그로 인해 얻는 결과는 그에 비해 매우 클 것입니다.
리액트 유저가 Hooks를 사용하면, 리액트의 여러 컨셉을 왔다 갔다 하며 사용하는 일이 없어질 것입니다. Hooks는 class, hoc, render-props와 같은 여러 개념 대신 함수만 사용할 수 있도록 해 줍니다.
리액트에서 Hooks 지원을 위해 사용하는 사이즈는 약 1.5kB밖에 되지 않습니다. 하지만, Hooks를 사용함으로 동일한 기능을 하는 코드의 양이 많이 줄기 때문에 bundle size를 더 많이 줄일 수 있습니다.
Hooks를 도입해도 기존 코드 베이스에 미치는 영향은 없습니다. 새로운 컴포넌트에만 Hook을 도입해도 기존의 코드는 그대로 작동합니다. 사실, 이러한 방식은 우리가 추천하는 방식입니다. 기존 코드에 불필요하게 시간을 들여 큰 수정을 하지는 말아주세요. 하지만, 핵심적인 코드에 Hooks를 도입해 보는 것은 좋은 방법이라고 생각합니다.
리액트 앱에서 로직을 재사용하는 방벙은 여러 가지가 있습니다. 함수를 하나 만들어서 사용할 수 있습니다. 함수형 또는 클래스형 컴포넌트를 만들어 사용할 수도 있습니다. 컴포넌트는 강력하지만, UI를 렌더해야만 합니다. 이것인 비주얼적인 로직이 아닌 코드를 공유하는데 어렵게 만듭니다. 이러한 이유로 우리는 render-props, higher-order components와 같은 개념을 도입하게 되었습니다. 리액트에서 코드를 재사용하는 일반적인 방법이 있다면 프로그래밍이 좀 더 간결해지지 않을까요?
함수는 코드를 재사용하기 매우 좋은 방법입니다. 하지만, 함수는 리액트 State를 가질 수 없습니다. 우리는 클래스 컴포넌트에서 Observables 같은 추상화된 개념을 사용하지 않고는, "윈도우 사이즈에 따른 State 변경", "시간에 따른 애니메이션 주기" 등과 같은 작업을 따로 땔 수 없습니다.
Hooks를 사용하면 정확히 위의 문제를 해결하고, 함수에서 리액트의 state, lifecycle, context와 같은 기능을 사용할 수 있습니다.
Hooks는 일반적인 자바스크립트 함수이기 때문에, 우리는 리액트에서 제공하는 Hooks를 이용해 Custom Hooks를 만들 수 있습니다. 덕분에 리액트 앱 내의 복잡한 개념을 함수에 담아 커뮤니티에 공유하기도 수월해졌습니다.
Custom Hook은 리액트의 기본 기능은 아니기 때문에, Custom Hook에 대한 가능성은 여러분이 어떻게 디자인하는지에 달려있습니다.
우리가 현재 윈도우 width를 subscribe하는 컴포넌트를 만든다고 가정해 보겠습니다. (예를 들면, 좁은 viewport에서 다른 콘텐츠를 보여주기 위해서)
이러한 코드를 작성하는 여러가지 방법이 있습니다. 클래스 컴포넌트에서, lifecycle 메소드를 작성하거나, render-prop이나 higher-order component를 이용할 수 있습니다. 하지만 아래와 같은 코드가 가장 좋다고 생각합니다.
function MyResponsiveComponent() {
const width = useWindowWidth(); // Custom Hook
return <p>Window width is {width}</p>;
}
위 코드는 앞서 언급한 동작을 실행하는 코드입니다. 우리는 width를 컴포넌트 안에서 사용했고, React는 width가 변경되면 컴포넌트를 re-render합니다. 컴포넌트가 state나 side effect를 가지고 있어도, 선언적으로 작성할 수 있도록 하는 것인 Hooks가 지향하는 것입니다.
custom Hook을 어떻게 구현했는지 살펴보도록 하겠습니다. 우리는 현재 window의 width를 주시하기 위해 React의 local state를 사용하고, 윈도우 width 변경에 따라 state를 수정하기 위해 side effect를 이용합니다.
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 width;
}
위와 같이, React Hooks의 useState, useEffect는 building block을 형성합니다. 우리는 이것을 컴포넌트에서 직접 사용할 수도 있고, useWindowWidth와 같은 custom hook과 결합해 사용할 수도 있습니다.
built-in Hooks에 대해서는 이곳에서 확인할 수 있습니다.
Hooks는 온전히 캡슐화되어 있습니다. Hook을 호출할 때마다, 현재 실행 중인 컴포넌트 내에서 독립적인 local state가 형성됩니다. 위의 예에서 window width는 어떠한 컴포넌트에서건 같기 때문에 상관이 없지만, Hooks의 이러한 특징은 매우 강력합니다. Hooks는 state를 공유하는 것이 아닌, stateful logic을 공유하는 방법입니다. 우리는 top-down data flow 규칙을 어기지 않고 이를 이용할 수 있습니다.
각 Hook은 local state와 side effect를 가질 수 있습니다. 함수에서와 같이 Hooks간 데이터를 전달할 수도 있습니다. argument를 취할수도 있고 값은 반환할 수도 있습니다. 일반적인 자바스크립트 함수기이 때문입니다.
아래는 Hooks를 이용해 React animation을 구현한 예입니다.
위 소스 코드를 보면, 같은 render function 안에서 Custom Hook에 값을 전달함으로써 애니메이션을 트리거합니다.
// Staggering animation with react-spring
const [{ pos1 }, set] = useSpring({ pos1: [0, 0], config: fast });
const [{ pos2 }] = useSpring({ pos2: pos1, config: slow });
const [{ pos3 }] = useSpring({ pos3: pos2, config: slow });
(위 예시에 대해 좀 더 알고 싶으시면, 이 튜토리얼을 참고해 주세요.)
Hooks의 우선적인 목표는 아니지만, Hooks는 강력하고 interactive한 디버깅 툴에도 가능성을 열어두었습니다.
Hooks 간 데이터를 주고 받을 수 있기 때문에 이는 애니메이션, 데이터 구독, Form 관리, stateful abstraction과 같은 작업에 매우 유용합니다. Render-props, HOC와는 다르게, Hooks는 “false hierarchy”를 생성하지 않습니다.
리액트 Hooks에서 Custom Hook은 가장 매력적인 부분이라고 생각합니다. Custom Hook을 작동시키기 위해서, 리액트는 state와 side effect를 선언할 수 있는 함수를 제공해야 합니다. useState, useEffect와 같은 built-in Hooks의 역활이 바로 이것입니다. 이곳에서 좀 더 살펴볼 수 있습니다.
built-in Hooks는 custom Hooks를 만드는데 유용할 뿐만 아니라, 컴포넌트를 선언하기에도 충분합니다. state와 같은 기능을 제공해 주기 때문입니다. 이 점이 우리가 Hooks가 향후 컴포넌트를 선언하는데 가장 우선적인 방법이 될 것이라고 생각하는 이유입니다.
Class를 deprecate 할 계획은 없습니다. 페이스북에도 현재 무수히 많은 class 컴포넌트가 있으며, 여러분과 같이 우리는 이 컴포넌트를 다시 작성하지 않을 것입니다. 리액트 커뮤니티에서 Hooks를 수용한다면, 컴포넌트를 작성하는 두 가지 추천 방법을 가지는 것은 맞지 않는다고 생각합니다. Hooks는 클래스 기반 컴포넌트의 사용 범위를 모두 커버하며, 코드의 재사용, 테스팅, extracting 적인 면에서 더 많은 유연성을 제공합니다. 이것이 리액트의 미래에 대한 비전을 Hooks가 담당하는 이유이기도 합니다.
여러분은 아마도 Rules of Hooks를 보고 놀라셨을 수도 있습니다.
Hooks가 top level에서만 호출돼야 하기도 하지만, 조건에 따른 state를 선언할 수도 없습니다. 예를 들어, class에서도 state를 조건에 따라 선언할 수는 없습니다. 그리고 지난 몇 년간 이에 대한 React 유저의 컴플레인 또한 없었습니다.
이러한 디자인은 Custom Hook이 문제없이 잘 작동하기 위해서 매우 중요합니다. 위와 같은 규칙이 익숙하지는 않겠지만, 이로 인해 오는 장점에 비하면 가치가 있다고 생각합니다. 이에 대해 동의하지 않더라도, 저는 일단 한 번 사용해 보기를 권하며, 사용해 본다면 생각이 달라질 수도 있을 것입니다.
우리는 위 규칙이 엔지니어를 혼란스럽게 하는지 살펴보기 위해서 production 레벨에서 한 달간 Hooks를 사용해 봤습니다. 그리고 몇 시간 안에 익숙해지는 것을 확인했습니다. 개인적으로 저도 이 규칙이 처음에는 옳지 않다고 느껴졌지만, 차츰 극복하게 되었습니다. 그리고 이 경험은 리액트에 대한 저의 첫인상을 떠올리게 했습니다. (여러분은 리액트를 접하고 즉각 좋다고 생각했습니까? 저는 두 번째 사용하기 전까지 그렇지 않았습니다.)
Hooks에 "magic"은 존재하지 않으며, 그 구현은 아래의 코드와 유사합니다.
let hooks = null;
export function useHook() {
hooks.push(hookData);
}
function reactsInternalRenderAComponentMethod(component) {
hooks = [];
component();
let hookForThisComponent = hooks;
hooks = null;
}
코드를 살펴보면 컴포넌트별 Hook list를 가지고 있고, Hook이 사용될 때마다 리스트의 다음 아이템으로 이동합니다. Hook에 대한 규칙으로, 매 렌더마다 리스트의 순서가 같으며, 매 호출 시 컴포넌트에 맞는 state를 전달할 수 있습니다. 리액트는 어떠한 컴포넌트가 지금 렌더링 중인지 파악하기 위한 어떠한 작업도 하지 않습니다.
(이 글은 훌륭한 시각적 자료와 함께 설명한 자료입니다.)
아마 여러분은 리액트 Hooks에서 state를 어디에 보관하는지 궁금할 수 있습니다. 이에 대한 답변은 class 컴포넌트에서와 같은 장소라는 것입니다. 리액트는 내부적으로 update queue가 있으며, 어떠한 state도 이곳에서 비롯됩니다.
Hooks는 최근 자바스크립트 라이브러리에서 일반적인 Proxies나 getters에 의지하지 않습니다. 그래서 개인적인 주장은 유사한 문제에 접근하는 유행하는 접근법보다 Hooks는 매직이 덜 합니다. Hooks에서 magic이라고 한다면 array.push와 array.pop이 전부입니다. (이를 위해서 호출 순서가 중요합니다~!)
Hooks 디자인은 React에 종속되어 있지 않습니다. 실제, Hooks에 대한 proposal이 발표되고 나서, 여러 분들이 Vue, Web Components, Vanilla 자바스크립트에서의 Hooks API에 대한 실험적인 구현을 구상했습니다.
여러분이 functional programming 순수주의자이고, 가변적 state에 의존하는 React에 대해 우려를 느낀다면, Hooks는 algebraic effect와 같은 방식으로 구현되기 때문에 이에 대해서는 만족감을 느낄 수 있을 것입니다. 물론 React는 내부적으로 가변적인 state에 의존합니다. (엄밀히 말하며, 여러분이 그 작업을 할 필요가 없도록 하기 위해서이지만)
Hooks에 대해 더 궁금한 점이 있다면, Hooks의 저자 Sebastian이 RFC에서 여러 염려에 대해 답변을 남긴 것을 참고해도 좋을 것입니다. 무엇보다도, Hooks는 더 적은 노력으로 컴포넌트를 작성할 수 있게 도와주며, 개인적으로 제가 Hooks를 좋아하는 이유이기도 합니다.
아직 Hooks가 흥미롭게 여겨지지 않아도 충분히 이해를 할 수 있지만, 작은 프로젝트에서라고 한 번 시험 삼아 사용해 보는 것을 추천합니다. Hooks가 해결하는 문제를 맞닥뜨리거나, 다른 해결책이 있다면 RFC에서 공유해 주시면 감사하겠습니다.
이 글이 여러분의 관심을 끌거나 최소한 궁금증을 자아냈다면, 그것은 이 글의 쓴 저로서는 만족합니다. 그리고 저는 단 한 가지 부탁이 있습니다. 지금도 리액트를 배우고 있는 많은 분들이 있습니다. 만약 우리가 나온 지 얼마 되지 않은 기능에 대해 서둘러 튜토리얼을 만들고 best practice에 관해 언급한다면, 아마 혼란이 생길 수도 있습니다. React팀 내에서도 아직 Hooks에 대해 확실하지 않은 부분이 있습니다.
Hooks가 아직 조금은 불안정한 상태에서 사용한다면, 그에 대해 명시를 해주고 공식 문서로 연결되는 링크를 추가해 주세요. Hooks에 대한 이해를 위해 큰 노력을 하고 있고, 여러 질문에 대한 답변도 준비돼 있습니다.
아직 Hooks에 대해 흥미가 없는 사람을 대해도 공손한 태도를 유지해 주면 좋을 것 같습니다. Hooks에 대한 오해가 있고 오픈 마인드라면, 여러분의 정보를 공유해 줄 수 있을 것입니다. 하지만, 어떠한 변화도 거부감을 조성할 수 있습니다. 우리는 사람들이 소외감을 느끼지 않도록 최선을 다할 것입니다.
리액트 공식문서에서 Hooks를 확인해 주세요.
Hooks는 아직 초기 단계에 있지만, Hooks에 대한 feedback은 저희에게 매우 소중합니다. 여러분은 RFC에 직접 작성할 수도 있고, 저희는 트위터에서 이에 대한 소통을 활발히 하기 위해 노력하고 있습니다.
위 내용 중 불명확한 부분이 있다면 말씀해 주세요. 이 글을 읽어 주셔서 감사합니다.