본문 바로가기

[리액트] 컴포넌트와 Hook

"리액트에서 hook이 왜 등장했을까요?"

 

"클래스 컴포넌트와 hook의 관계는 뭐라고 생각하세요?"

 

 

최근 받았던 이 질문들에 대한 답변을 잘 하지 못했다.

 

React 사용자가 아니라 React 개발자가 되기 위해서는 컴포넌트를 제대로 알 필요가 있었다.

 

2021.02.11. 업데이트

(다시 읽어보니 좀 빈약해서 ^^;)

리액트 16.8 이전까지는 상태 관련 로직을 재사용하기 위해서는 render props, HOC를 사용하였으나 이는 많은 Wrapper컴포넌트 구조를 가지게 되어 코드 구조를 파악하기 어렵게 만듭니다. Hook은 리액트에서 상태관련 로직을 더 쉽게 공유하고 재사용하기 위해 고안되었습니다.

React는 컴포넌트간에 재사용 가능한 로직을 붙이는 방법을 제공하지 않습니다. (예를 들어, 스토어에 연결하는 것) 이전부터 React를 사용해왔다면, render props이나 고차 컴포넌트와 같은 패턴을 통해 이러한 문제를 해결하는 방법에 익숙할 것입니다. 그러나 이런 패턴의 사용은 컴포넌트의 재구성을 강요하며, 코드의 추적을 어렵게 만듭니다. React 개발자 도구에서 React 애플리케이션을 본다면, providers, consumers, 고차 컴포넌트, render props 그리고 다른 추상화에 대한 레이어로 둘러싸인 “래퍼 지옥(wrapper hell)“을 볼 가능성이 높습니다. 
개발자 도구에서 걸러낼 수 있지만, 이 문제의 요점은 심층적이었습니다. React는 상태 관련 로직을 공유하기 위해 좀 더 좋은 기초 요소가 필요했습니다.

Hook을 사용하면 컴포넌트로부터 상태 관련 로직을 추상화할 수 있습니다. 이를 이용해 독립적인 테스트와 재사용이 가능합니다. 
Hook은 계층의 변화 없이 상태 관련 로직을 재사용할 수 있도록 도와줍니다. 이것은 많은 컴포넌트 혹은 커뮤니티 사이에서 Hook을 공유하기 쉽게 만들어줍니다. [Hook의 개요 발췌]

 


1. React에서 컴포넌트는 뭘까?

다음은 react package에서 Component를 정의한 부분이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Component(props, context, updater) {
  this.props = props;
  this.context = context; // If a component has string refs, we will assign a different object later.
 
  this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the
  // renderer.
 
  this.updater = updater || ReactNoopUpdateQueue;
}
 
Component.prototype.isReactComponent = {};
 
Component.prototype.setState = function (partialState, callback) {
  (function () {
    if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
      {
        throw ReactError(Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables."));
      }
    }
  })();
 
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
 
Component.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
 

오 보다시피 Component는 생성자 함수로써 우리는 클래스 컴포넌트를 사용할 때 아래처럼 Component 생성자를 extends해서 사용했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { Component } from 'react';
 
class App extends Component {
  constructor (props) {
    super(props);
  }
 
  render () {
    return (
      <h1>Hello, This is {this.props.name}.</hl>
    }
  }
}
 
export default App;

여기서 Component란 입력값으로 props와 state를 받아 React Element를 출력하는 함수라는 것을 이해할 수 있었다.

(참고! 여기서 Element는 DOM Element와는 다르다. 최근에 번역한 이 글에서 알 수 있다.)

 

실제로 React Document에서도 다음과 같이 컴포넌트를 정의한다.

Conceptually, components are like JavaScript functions. They accept arbitrary inputs (called “props”) and return React elements describing what should appear on the screen.

 

2. UnControlled vs Controlled

Controlled 컴포넌트는 React에 의해서 상테를 제어할 수 있는 컴포넌트를 말한다.

우리는 React state를 “신뢰 가능한 단일 출처 (single source of truth)“로 만들어 두 요소를 결합할 수 있습니다. 그러면 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어합니다. 이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다.
[출처] https://ko.reactjs.org/docs/forms.html#controlled-components

Uncontrolled 컴포넌트는 React에서 상태를 제어하지 않는 컴포넌트를 말한다. 쉽게 생각하면 클래스 컴포넌트에서 state가 없는 컴포넌트가 그러할 것이다. Uncontrolled 컴포넌트는 render가 발생하지 않는다는 장점이 있지만, 사용자 상호작용으로 화면을 업데이트하는 게 일반적인 개발 환경에서는 사용되지 않는다.

 

그러나! 여기에서 확인할 수 있듯이 key 프로퍼티와 함께 사용하여 불필요한 렌더링을 막는 방법으로 활용할 수 있다.

 

혹은 ref를 사용하여 DOM Element를 직접 접근하여 값을 사용하는 방식으로 Controlled 컴포넌트에서 state를 사용하는 방식과 유사하게 구현할 수 있다. state와의 차이점은 이 글을 참고하자 :)

 

그리고, UnControlled 컴포넌트를 이렇게 사용하기 보다는 PureComponent를 통하여 원하는 값이 변경되었을 때만 render를 실행하게 하는 것이 컴포넌트를 최적화하는 더 나은 방법일 수도 있다.

Component는 항상 render를 다시 실행하지만 PureComponent는 Props나 State를 얕은 비교해서 변경점이 없으면 render를 다시 실행하지 않아요.

 

3. PureComponent 사용 시 주의할 점

PureComponent는 props 혹은 state의 비교를 통해 render 여부를 결정하는데, props에 다음의 값을 넘기면 PureComponent를 사용하는 의미가 없어진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render () {
  <>
  <Text
    onClick={text => { console.log(text) }} // 인라인 함수
  />
  <Text
    value={new String(value)} // 생성자 함수
  />
  <Text
    component={<SubText />// 컴포넌트
  />
  <Text
    object={{ name"juj" }} // 객체
  />
  </>
}
 

Text 컴포넌트가 PureComponent라고 가정하고, Text 컴포넌트를 render() 내에서 저렇게 props를 설정한다면, render()메소드가 실행될 때마다 해당 인라인 함수, 생성자 함수, 컴포넌트, 객체를 새로 생성하기 때문에 props가 매번 변경되었다고 인식하고 매번 render()할 것이다.

 

4. Stateful vs Stateless

단어 뜻만 풀이하자면 state가 있는 컴포넌트와 state가 없는 컴포넌트라고 구분지을 수 있을 것이다.

 

state의 유무는 곧 데이터 변화를 추적할수 있느냐 없느냐의 차이이다.

 

우리는 보통 컴포넌트를 만들 때 부모 컴포넌트 (혹은 Wrapper 컴포넌트)에 동적으로 변하는 데이터를 모두 state로 정의하고, 자식 컴포넌트는 필요한 정보만을 props로 받아 사용한다. 이 때 wrapper 컴포넌트가 곧 stateful 컴포넌트(==Container==Smart)이고, 자식 컴포넌트를 Stateless 컴포넌트(==Presentational==Dumb)라고 칭한다.

 

보통 Stateless 컴포넌트는 state가 존재하지 않기 때문에 함수형 컴포넌트로 표현한다.

 

보통 함수형 컴포넌트를 사용하면 다음의 장점이 있다.

  • 함수형 컴포넌트는 클래스 컴포넌트와 달리 state나 lifecycle이 없는 일반 자바스크립트 함수이기 때문에 읽고 테스트하기 쉽다.
  • Best Practice로 사용하기 좋다. 컴포넌트의 state를 사용해야 할 부분과 사용하지 않아도 되는 부분을 구분하기 쉬워지기 때문에 Container/ Presentational 컴포넌트로 분리하기 쉬워진다.
  • 리액트 개발팀에서 말한 바에 따르면, 함수형 컴포넌트의 성능이 더 좋다고 한다. (Hook을 발표하고 나서, 인스턴스를 새로 생성할 필요가 없어 성능이 더 좋다고 말함)

 

5. Hook은 어떤 Needs로 등장했을까?

지금까지 컴포넌트란 무엇이고, 컴포넌트를 분류하는 기준은 어떤 것이 있는지를 공부했다.

 

그렇다면 16.8부터 등장한 React Hook은 전통적인 컴포넌트 개발 방식에서 어떤 불편함이 있어서 제안되었는지를 리서치했다.

 

먼저 Hook이 정식 버전으로 릴리즈되기 전에 리액트 개발자인 Dan Abramov가 쓴 Making Sense of React Hooks에서 그 답을 찾아보았다.

 

글을 세 번 읽으면서 요약해본 바는 다음과 같다.

컴포넌트는 Top-down 데이터 흐름으로 거대한 UI를 작고 독립적이고, 재사용할 수 있는 단위로 분리할 수 있었다. 그러나 비즈니스 로직이 Stateful하고 함수나 다른 컴포넌트로 분리하기 어려운 경우가 있기 때문에 여전히 복잡한 컴포넌트가 존재했다.

Hook은 다음과 같은 상황을 해결하는 데 도움이 될 것이다.
- 리팩토링과 테스트하기조차 어려울 정도로 거대한 컴포넌트
- 다른 컴포넌트 혹은 라이프 사이클에서 반복적으로 사용되는 로직
- 코드 재사용을 위한 복잡한 패턴들(render props, HOC)

Hook은 컴포넌트 안의 로직을 재사용가능한 독립적인 단위로 구성할 수 있게끔 해준다.

그렇다면 Hook은 무엇일까?

전통적인 React에서, 컴포넌트와 UI와 관련이 없는(non-visual) 비즈니스 로직을 결합하는 방식으로 render props, HOC를 사용하였다.

그러나 코드 재사용을 위한 통합된 방법이 없을지 고민하게 되었고, 자바스크립트 함수가 코드 재사용에 적합해보였다.

그러나, 함수에는 클래스 컴포넌트의 local state나 lifecycle이 없다. 그래서 Hook에는 한 번의 함수 호출로 이것을 해결할 내장 hook들을 제공한다. (useState, useEffect 등)

Hook은 자바스크립트 함수이기 때문에 이러한 내장 hook들을 Custom hook에서 조합하여 사용할 수 있다.

또한 render props나 HOC와는 달리 'false hierarchy'를 생성하지 않고, 마치 memory cells 리스트가 컴포넌트에 추가된 형태라 독립적이고 어느 컴포넌트에서나 재사용할 수 있다.

React 개발팀에서는 클래스 컴포넌트를 deprecate하지는 않겠지만 Hook은 클래스 컴포넌트의 모든 특징을 포함하는 동시에 코드를 추출, 테스트, 재사용하는 데 더욱 유연하다. 이것이 Hook이 리액트의 앞으로의 비전을 표현하는 이유이다.

훅의 개념은 다음 코드에서 볼 수 있듯이 정말 단순하게도 각 컴포넌트마다 hook array를 두어 hook이 사용될 때마다 다음 인덱스의 hook을 실행한다고 설명할 수 있다.

이 뜻은 곧 Hook은 기존 React에서 사용하던 개념이나 원리를 크게 바꾸지 않는다는 의미로, 여러분이 Hook을 배우기 쉬울 것이다.

요약을 해도 좀 길긴 하지만 글의 굵은 글씨가 곧 Hook이 등장한 이유였다.

 

Hook은 컴포넌트에서 한 덩어리로 된 비즈니스 로직을 쪼개서 재사용 가능한 함수로 만들 수 있는 패러다임으로 등장하였다.

 

6. Hook을 사용하면 얻는 이점

이러한 필요로 인해 등장한 Hook으로 인해 얻는 이점은 어떤 것들이 있을까?

 

6.1 컴포넌트의 State 관리 측면에서, Hook으로 작성한 코드는 더욱 선언적(declaratively)이라 가독성이 좋다.

예컨대 클래스 컴포넌트에서의 LifeCycle을 보면 상관관계가 없는 로직들이 componentDidUpdate()안에 함께 존재하였지만 Hook에서는 이를 useEffect로 분리할 수 있다. 이렇게 분리된 로직들은 관리하기 더욱 유용하다.

 

게다가 Custom Hook으로 모듈을 분리하여 다른 컴포넌트에서도 재사용할 수 있기도 하다.

One of the greatest things about React is its declarative nature. While this is mostly true, the imperative setState (and possibly lifecycle methods) are actually a deviation from this core characteristic. With hooks, code can be written more declaratively with almost no branches, making it easier to follow.
[출처] https://blog.bitsrc.io/why-we-switched-to-react-hooks-48798c42c7f

 

6.2 다양한 내장 Hook들을 제공한다.

기존의 Class Component와 함께 사용하던 Context API 혹은 redux를 사용하던 방식을 크게 바꾸지 않고도 Hook으로 전환할 수 있는 내장 Hook들이 존재한다.

 

또한 이 내장 Hook들을 꼭 컴포넌트에서만 사용가능한게 아니라 non-visual 로직에서도 활용하여 커스텀 훅을 만들 수 도 있으니 유연함이 장난아니다.

 

1. Context API 관련: useContext

2. Redux 관련: useReducer, useDispatch, useSelector

3. Router 관련: useHistory, useParams, useLocation

 

6.3 코드 재사용성이 뛰어나다.

등장 배경에서도 계속 말하고 있지만, 재사용성이 정말 뛰어나다.

 

실무에서는 사용자 컴퓨터에서 이미지를 첨부하면 썸네일을 띄우는 커스텀 훅 useImage을 만들었다.

 

useImage Hook은 빌더 웹 설정에서도, 블록 이미지, 웹챗 버튼 이미지 설정 등등 이미지를 첨부할 수 있는 컴포넌트 곳곳에서 사용되고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import React from "react";
import toast from "../utils/toast";
 
const useImage = _ => {
  const uploadImage = id => {
    return document.getElementById(id).click();
  };
 
  const initFile = e => {
    e.target.value = null;
  };
 
  const chkValidImage = files => {
    const validTypes = ["image/png""image/jpeg""image/jpg"];
    const file = first(files);
 
    if (!file) return false;
 
    if (!validTypes.includes(file.type)) {
      return toast.error(
        <span>
          이미지 형식이 아닙니다.
          <br />
          png, jpeg, jpg확장자만 사용가능합니다.
        </span>
      );
    } else if (file.size > 307200) {
      return toast.error("이미지 파일의 최대 크기는 300KB입니다.");
    }
 
    return true;
  };
 
  const loadThumbnail = curry((f, e) => {
    e.preventDefault();
    let reader = new FileReader();
    let file = e.target.files[0];
    if (e.target.files[0]) {
      reader.onloadend = _ => {
        f(file, reader.result);
      };
      reader.readAsDataURL(file);
      return true;
    } else {
      e.target.value = null;
      return false;
    }
  });
 
  const onChange = curry((setImage, e) => {
    const isValidImage = chkValidImage(e.target.files);
 
    if (!isValidImage) {
      return false;
    } else {
      loadThumbnail(setImage, e);
      return true;
    }
  });
 
  return [uploadImage, initFile, onChange];
};
 
export default useImage;
 
 

 

7. 결론

이번 글은 컴포넌트를 어떻게 하면 더 잘 쓸 수 있을지, 그리고 서두의 질문을 또 받게 된다면 나는 뭐라고 답할지 계속 리서치하면서 얻은 정보들이다.

 

[출처] 내 손

결국 hook은 함수형 컴포넌트처럼 생겼지만, 용도면에서 보았을 때에는 구분지어졌고, 클래스와 함수형 컴포넌트가 할 수 있는 모든 일을 hook에서 할 수 있었다.

 

이 글에서 클래스 컴포넌트와 함수형 컴포넌트는 '전혀 다른 포켓몬'이라고 할 수 있다고 언급한 것처럼 Hook도 사실은 이 둘을 통합한 하나의 방법을 제시하고 싶어서 나온 다른 포켓몬이 아닐까?

 

 

[참고 자료]

https://velog.io/@vies00/React-Hooks

https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/

https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889

https://hyunseob.github.io/2019/06/02/react-component-the-right-way/

https://medium.com/@Zwenza/functional-vs-class-components-in-react-231e3fbd7108

https://blog.bitsrc.io/why-we-switched-to-react-hooks-48798c42c7f