About React Hook

MinuKang
Vingle Tech Blog
Published in
12 min readMar 8, 2019

안녕하세요, Vingle Frontend 개발팀입니다.

React 16.8 버전부터 아주 중요한 이벤트가 있습니다. 바로 Hook 이라는 개념이 정식으로 도입이 되었는데요, 이게 도대체 뭔지, 왜 중요한건지, 빙글에서는 이 변화를 어떻게 받아들여였는지, 개발자들이 일하는 방식에 어떤 변화를 가져오는지 살펴보려고 합니다 :)

Hook 이란?

Hook이란 함수형 Component에서 State, LifeCycle, Reference 등의 클래스형 Component의 기능의 구현한 개념입니다. 또한 함수형 Component의 특성상 쓰기 어려웠던 Memoization등의 기능 또한 포함되어 있습니다.

Hook state 사용하기

기존의 React의 함수형 Component는 클래스 Component와 다르게 State와 LifeCycle API를 사용할 수 없었습니다.

그렇기 때문에 이 함수형 Component는 간단한 display만을 위한 Component를 만드는 용도로 한정되어 사용되어 왔습니다. 하지만 Hook이 추가되면서 함수형 Component가 좀 더 다양한 일을 할 수 있게 되었습니다.

간단한 Counter Component 예제입니다. 이걸 그대로 함수형 Component로 옮길 수 있을까요? 기존에는 State를 대체할수 없기 때문에 불가능했었죠.
이제는 hook이 추가 되면서 다음과 같은 구현이 가능합니다.

좀더 살펴보겠습니다. 이 사용 예시에서 핵심 인터페이스는 useState는 다음과 같이 선언되어있습니다.

인자로 초기 값을 받습니다. 그리고 배열을 리턴하는데, 첫번째는 state, 두번째 state를 변경 할 수 있는 함수를 사용 할 수 있습니다. 그 외에 초기 값을 콜백으로 설정할 수도 있고, 디스패치할 때 직접 접근하는 것이 아니라 이전 값을 참조할 수 있는 콜백으로도 사용 할 수 있는 등 사실상 기존 State 와 동일한 기능을 합니다.

ES6를 쓰신다면 위와같이 깔끔한 변수 할당이 가능합니다.

Hook effect 사용하기

Hook 을 통해 함수형 컴포넌트는 LifeCycle을 구현할 수 있습니다. 이전 카운팅 컴포넌트에 새로운 기능을 추가 해보겠습니다.

마운트 되었을 때 기존의 문서 제목을 저장해두고 새로운 제목을 설정하고, 카운팅이 업데이트 될 때 마다 제목을 갱신합니다. 그리고 컴포넌트가 마운트 해제되면 기존 제목으로 되돌립니다. 보시면 알겠지만 componentDidMount 하고 componentDidUpdate 이 똑같은 이벤트를 발생 시킵니다. 함수형 컴포넌트에서 Hook을 사용한다면 다음과 같이 할 수 있습니다.

useEffect를 통해 라이프사이클을 관리할 수 있습니다. 기본적으로 render가 실행되면useEffect가 함께 실행된다고 보시면됩니다. 그리고 리턴되는 콜백을 통해 componentWillUnmount를 구현 할 수 있습니다.

prevDocumentTitle은 후술할 React.useRef() 를 사용하여 클래스 인스턴스 처럼 구현할 수 있습니다.

지금은 간단하게 문서의 제목을 바꾸는 작업이라서 별다른 이슈가 없지만, 실제로는 모든 render 요청에 대해, useEffect가 실행되니 사실 이 method의 퍼포먼스는 중요합니다. 이를 위해useEffect의 두번째 인자로 변경 사항이 있는 값들을 배열로 보내어, 해당 값들이 변경될 때만 effect를 실행 하게끔 할 수 있습니다.

이렇게 설정함으로 count가 변경될 때만 effect를 실행하게끔 할 수 있습니다. 그리고 Hook으로 만들어진 state뿐만이 아니라 props의 값도 감시를 할 수 있습니다.

그 외 Hook API

useRef

기존 React.createRef() 기능을 그대로 Hook으로 구현한 React.useRef() 로 함수형 컴포넌트 내에서 컴포넌트 레퍼런스를 참조할 수 있게 됐습니다.

또한 단순히 DOM이나 컴포넌트를 참조하는 것이 아니라 클래스 컴포넌트의 인스턴스처럼 사용을 할 수 있습니다. 하지만 초기에만 실행 시켜줄 수 있는 콜백 형식으로 전달하지 못하기 때문에 아래와 같이 setInterval의 레퍼런스를 할당하고자 할 때 useEffect 등을 이용해서 별도로 설정해 주셔야 합니다.

useContext

Context API를 기존에 사용 할려면 다음과 같이 구현해야 했습니다.

문제는 Context 갯수만큼 wrapper를 만들어야 하는 구조라는 점입니다.

그래서 복잡한 application (여러개의 context를 사용하는)을 만들다 보면, 쉽게 Wrapper hell에 빠진 자신을 발견하게 됩니다.

ㅗㅜㅑ

하지만 새로 추가된 useContext를 사용한다면 다음과 같이 쉽게 구현할 수 있습니다.

훨씬 깔끔하게 구현이 되었습니다!

useMemo & useCallback

useMemo, useCallback은 memoize 를 함수형 컴포넌트에 사용할 수 있도록 만들어진 기능입니다. 둘의 사용처는 비슷한데요, 차이는 useMemo는 계산된 값을 가지고 있고, useCallback은 콜백으로 실행 할 수 있다는 차이가 있습니다. 두 API 다 첫 번째 인자로 값을 리턴하는 콜백을 가져오고, 두 번째 인자로 변경 여부를 평가하는 값들의 배열을 가져옵니다.

이를 통하여 함수형 컴포넌트 내에서 계산되는 값들이나 콜백에 대해서 최적화를 할 수 있습니다.

Hook 으로 구현할 수 있는 Helper

useEffect로 componentDidMount 구현

useState를 단방향 flag로 사용하여 useEffect내에서 한번만 실행하여 클래스 컴포넌트의 componentDidMount 만을 구현할 수 있습니다.

useRef 로 이전 값 가져오기

useRef의 업데이트 시점을 이용하여 componentDidUpdate 에서 사용할 수 있는 이전 값을 사용 할 수 있도록 할 수 있습니다.

Hook 사용시 유의사항

1. Hook은 함수형 Component 내에서만 작동된다.

Hook의 상태는 Component 단위로 관리됩니다. 따라서 반드시 Component안에서 사용되어야 합니다.
또한 클래스 Component에서는 아예 사용할 수 없도록 막혀있습니다. 의미가 없기 때문이죠.

2. 성능관리에 유의해야한다.

Hook을 사용하게 됨으로 함수형 Component는 기존의 제한된 display component에 국한되지 않고, 일반적인 클래스형 Component로 구현하는 거의 모든 것들을 구현할수 있게 됩니다.
따라서, 이렇게 복잡한 Component를 구현할때는 반드시 성능을 관리하여야 합니다.

Hook 최적화

1. React.memo 사용

React.memo는 함수형 컴포넌트에서 React.PureComponent 혹은 shouldComponentUpdate를 구현 가능하도록 만들어진 API입니다. 이미 Hook이 추가되기 전인 16.6에 추가가 되었습니다.

React.memo에 첫번째 인자로 컴포넌트를 넘겨서 사용해주면 React.PureComponent와 같이 변경되는 props의 얕은 비교를 하여 render의 실행여부를 결정하게 됩니다.

두번째 인자에 shouldComponentUpdate 의 평가 식을 보내주면 해당 평가 식으로 하여 렌더링 실행 여부를 결정하게 됩니다.

2. Render 내의 평가식 최적화

React.memo 를 통해 렌더링을 최소화 했다면, 이제 렌더링 안에 있는 평가식들을 고려해야 합니다. 다음과 같은 State를 선언하는 게 있고 초기 값에 사용하는 getManyHardUsingData 는 꽤나 비용이 발생하는 함수라고 가정합니다.

“value” 가 아닌 함수를 초기 값으로 설정하면 render가 실행될 때 마다 getManyHardUsingData 가 실행될 것 입니다. 어차피 Hook으로 인하여 해당 컴포넌트는 저장하고 있으니 매우 불필요한 평가식이죠. 다음과 같이 개선 할 수 있습니다.

단순히 해당 초기값을 콜백으로 바꿔줬을 뿐이지만, React에서는 해당 평가식을 처음에만 실행하고, 다음 render 부터는 실행하지 않습니다. state가 단순한 원시값이 아닌 함수나 객체 생성이라면 콜백을 통해 생성하셔야 합니다.

이를 통해 브라우저의 IO에 접근해서 읽는 비용이 큰 로컬스토리지, 세션스토리지를 초기값으로 사용할 수 있습니다.

Migration

빙글은 React hook을 이용하여 기존 서비스를 마이그레이션을 진행하였는데요, 대표적으로 Server Side Rendering을 할 때 스타일시트를 적용시켜주기 위해서 직접만들어서 사용하고 있는 withStyles HOC 입니다.

컴포넌트가 실행될 때 마다 함수를 실행시켜주기만 하면 되는데 Context에 저장되어 있는 값을 props로 받아와야 되었고 LifeCycle를 이용해야 되었기에 클래스로 만들 수 밖에 없었습니다.

StyleAttacher 로 인하여 컴포넌트 구조도 React.Fragment 아래에 2개의 노드가 있는 구조가 되었고 enzyme로 테스트할 때도 위와 같이 childAt(1)을 붙여서 테스트를 하고 있었기 때문에 개선해야되는 부분이라고 생각했습니다.

개선

위와 같이 단순히 Context API를 React.useContext로 바꿔주면 되지 않을까? 하였지만 이것은 잘못된 방법입니다. 그 이유는 Hook은 함수형 컴포넌트 이외에서 사용하면 작동을 하지 않습니다. 때문에 wrapWithStyles 함수 안에 있는 Hook은 정상적으로 실행되지 않을 겁니다.

위와 같은 케이스에서는 WithStyles가 함수형 컴포넌트이므로 그 안에서 Hooks를 사용하는 것이 맞습니다.

위와 같이 useContext을 이용해서 Context에 저장된 함수를 가지고 옵니다.

styles가 바뀔 때마다 insertCssFn을 호출해주어야 되기 때문에 useEffect를 추가해줍니다.

결론적으로 이렇게 마이그레이션을 할 수 있습니다. 하지만 아직 끝난게 아닙니다. 위에서 말했듯이 기존의 코드는 React.Fragment 안에 2개의 노드가 있는 구조라서 테스트 할 때 childAt을 붙여주었지만 단일 노드로 바뀌면서 삭제를 해야되고, 아직 enzyme에서 Hook 테스트를 지원하지 않아서 react-dom/test-utils에 있는 act 라이브러리 사용 해야 합니다.

위 문서를 참고해서 Hook 테스트를 진행해보세요!

Custom Hook

cookie를 기반으로한 userAuth 상태를 반환하는 hook을 만들어보겠습니다!

만들고자 하는 커스텀 Hook은 cookie에 있는 auth_token을 이용해서 현재 auth 상태를 반환&변환 시키고 이 Hook을 사용하는 모든 곳에서 같은 상태를 공유할 수 있도록 해보겠습니다.

아래 예제는 Typescript + React 16.8.3을 기반으로 작성 됐습니다.

커스텀 Hook을 만들어보고 싶으신 분은 아래 링크를 참고하여 만들어 보면 좋을것 같습니다.

Custom Hook guide document

백문이 불여일견!

Hook을 이용한 버전을 촬영했습니다.

굉장히 심플하죠?, 그렇다면 소스는 얼마나 더 심플할까요?

우선은 Hook을 사용하지 않은 코드를 보시죠

위 영상과 똑같은 기능을 하는 hook을 사용하지 않은 코드입니다.

아이디와 비밀번호를 기억해두기 위해서 클래스 컴포넌트를 이용하였고, 로그인 상태를 알기 위해서 getAuthToken() 을 이용해서 cookie 값을 받고 있습니다.

조금 걸리는것은 매번 getAuthToken(); 해줘야한다는 것이고, 로그인을 했다고해서 state가 바뀌는 것은 아니기에 SignIn 컴포넌트는 새로고침을 하지 않을겁니다.

그래서 location.reload() 를 이용해서 페이지를 새로고침 하는 해결법을 이용했습니다.

만약, redux를 이용했다면 store를 만들어서 location.reload() 를 이용한 강제 새로고침을 막을 것입니다.

자! 그럼 이제 Hook을 이용했을때 얼마나 문제들이 해결 되고, 코드가 간결해지는 보도록 하겠습니다.

<SignIn /> 컴포넌트가 말끔해졌습니다!

그리고 위에서 말했던 location.reload(); 을 지우기 위해서 굳이 redux를 추가하지 않아도 되겠네요! 또한 로그인 상태를 알기 위해서 매번 Boolean(getAuthToken()); 을 호출하던 것 도 사라졌네요 :)

결론

함수형 Component는 지금까지 아주 간단한 Displayer Component, 그 중에서도 stateless한 Component 로만 사용되어 왔습니다. 하지만 Hook의 출현으로 인하여 함수형 Component는 다양한 일을 할 수 있게 되었습니다.

저희는 클래스형 Component는 컨테이너 Component, 즉 많은 상태와 로직을 포함하고 있는 Component에 적합하다고 생각하고 있습니다. 다양한 메서드를 함수형 Component에 포함하기에는 가독성이 떨어지고, Domain과 Context를 분리하기 힘들어 유지보수하기 힘든 형태가 될 것이라고 생각되기 때문입니다.

반면에 함수형 컴포넌트는 Hook으로 인하여 이제 프레젠테이션 컴포넌트를 작성할 때 클래스형 컴포넌트를 대체 할 수 있다고 기대합니다. 또한 HOC(High Order Component)를 더욱 더 간결하게 작성할 수 있게 됩니다.

아직 시간이 얼마 되지 않아 테스트하는 방법이 부족하지만, 점차 나아질 것이라 믿고 있으며 오늘도 기존 소스에서 Hook으로 대체할 것을 찾아 다니고 있습니다.

Vingle Fron End 팀은 수많은 사람들이 매일 새로운 관심사와 커뮤니티를 발견하는 빙글을 만들어 나갑니다.

많은 지원 부탁드립니다!

--

--