# 리액트 hooks: 함수형 컴포넌트

률루랄라·2020년 1월 20일
1

1. 리액트 hooks 기초

1.1 리액트 훅이란?

클래스형 컴포넌트의 기능을 사용할 수 있는 함수형 컴포넌트라고 볼 수 있다.
life-cycle과 state관리 모두 가능하다.


1.1.1 혹의 장점

재사용 가능한 로직의 관리가 쉽다는게 제일 큰 장점이다. 이는 훅이 단순한 함수이고 함수 안에서 다른 함수를 호출하는 것으로 새로운 훅을 만들 수 잇기 떄문이다. 따라서 리액트의 내장 훅과 다른 사람들이 만든 여러 커스텀 훅을 레고처럼 조립해서 쉽게 새로운 훅을 만들 수 있다.

훅을 사용하면 같은 로직을 한곳으로 모을 수 있어서 가독성이 좋다. 반면 클래스형 컴포넌트의 생명 주기 매서드는 서로 다른 로직이 하나의 메서드에 섞여 있어서 가독성이 좋지 않다.

또한 정적 타입 언어로 타입을 정의하기 쉽다는 장점도 있다.

1.2 use state: 함수형 컴포넌트에 상탯값 추가하기

useState훅을 이용하면 함수형 컴포넌트에서도 상탯값을 관리할 수 있다.


import React, {useState} from 'react';

function Profile() {
  const [name, setName] = useState('');return (
    <div>
      <p> {`name is ${name}`}</p>
      <input type='text' value={name} onChange={e=> setName(e.target.value)} /></div>
    );
}


useState훅은 배열에 두 값을 넣어서 반환한다.
배열의 첫 번째 원소는 상탯값 즉 state인데 함수 호출 시 입력한 인수가 초깃값으로 사용된다. 두 번째 원소는 상탯값을 변경할 수 있는 함수이다.
배열의 비구조화 할당 문법을 이용해서 각 원소에 이름을 부여한다. 이처럼 useState 훅을 통해서 함수형 컴포넌트에서도 상태값을 사용할 수 있다.


사용자가 키보드를 누를 때마다 setName 함수를 호출한다. onChange 속성값으로 입력되는 함수는 렌더링이 될 때마다 생성되므로 성능면에서 걱정될 수 있다. 이 문제의 해결을 위해 useCallback훅을 제공한다.

1.2.1 여러 상태값 하나로 관리하기

useState 훅 하나로 여러 상태값을 하나의 객체에 담아서 관리할 수 있다.

import React, {useState} from 'react';

function Profile () {
 const [state, setState] = useState({ name: "", age:0});return (
   <div>
      <p> {`name is ${state.name}`}</p>
      <p> {`age is ${state.age}`}</p>
      <input 
        type='text' 
        value={state.name} 
        onChange={ e=> setState({ ...state, name:e.target.value})}/>
      <input
        type='number'
        value={state.age}
        onChange={ e=> setState({ ...state, age:e.target.value})}/>
   </div>
    )
}


두 개의 상태값을 하나의 객체로 관리한다


기존 클래스형 컴포넌트에서의 setState메소드는 기존 상태값과 입력된 값을 병합하지만
useState 훅은 이전 상태값을 지운다.
따라서 ...state와 같은 코드가 필요한 것이다.
더 나아가 하나의 객체로 상태값을 관리할 때 useReducer 훅이 제공된다.

1.3 Life-cycle 함수 사용하기 : useEffect

useEffect 훅을 사용하여 함수형 컴포넌트에서도 life-cycle 함수를 이용할 수 있다.
클래스형 컴포넌트에서의 각각 생명주기 메소드에 대응하는 훅이 존재하는 것은 아니지만 useEffect 훅을 이요하면 비슷한 기능을 한곳으로 모을 수 있어서 가독성이 좋아진다.


API 호출하는 기능과 이벤트 처리 함수를 등록하고 해제하는 기능을 각각 훅으로 구현하고 두기능을 하나로 합쳤을 때 함수형 컴포넌트와 클래스형 컴포넌트의 모습들을 비교할 것이다.

그 전에 useEffect 훅의 사용법을 예제로 살펴보자

import React, ( useState, useEffect } from 'react';

function MyComponent() {
  const [count, setCount ] = useState(0);
  useEffect( ()=> {
    document.title = '업데이트 횟수 : ${count}`;
  });
  return <button onClick={ ()=> setCount(count +1) }> increase </button>
}


useEffect 훅에 입력된 함수는 렌더링 결과가 실제 돔에 반영된 후 호출된다.
따라서 document.title을 변경하는 코드를 클래스형 컴포넌트의 componentDidMountcomponentDidUpdate 양쪽 메소드에 추가하면 같은 기능을 구현하게 될 것이다. 즉 class형 컴포넌트에서는 서로 다른 로직이 하나의 생명주기 메소드에서 작성됐지만, useEffect훅을 사용하면 하나의 로직을 위한 코드를 한 곳으로 모을 수 있다.

버튼을 클릭하면 다시 렌더링되고 렌더링이 끝나면 useEffect훅에 입력된 함수가 호출된다.

1.3.1 API를 호출하는 기능: 함수형 컴포넌트에서

import React, ( useState, useEffect } from 'react';

function Profile({userID }) {const [user, setUser] = useState(null);useEffect (
    ()=> {
      getUserApi(userId).then(data => setUser(data) );
    },
    [userId],);
  return (
    <div>
      {!user && <p>사용자 정보를 가져오는 중...</p>
        { user $$ (
          <>
            <p>{`name is ${user.name}`} </p>
            <p>{ `age is ${user.age}`} </p>
         </>
          )}
    </div>
}


API 결과값을 저장할 상탯값이다.

useEffect 훅에서 API 통신을 하며, 받아온 데이터는 user상태값에 저장한다.

useEffect 훅에 입력된 함수는 렌더링할 때마다 호출되기 때문에 API통신을 불필요하게 많이 하게 된다. 이를 방지하기 위해 useEffect 훅의 두 번째 매개변수로 배열을 입력하면, 배열의 값이 변경되는 경우에만 함수가 호출된다. 여기서는 useId값이 변경되는 경우에만 API통신을 하도록 설정한다.

1.3.2 API를 호출하는 기능: 클래스형 컴포넌트에서

import React from 'react';

class Profile extends React.Component {
 state = {
  user: null, 
 };
componentDidMount() {const { userId } = this.props;
  getUserApi(userId).then(data => this.setState({ user:data}));
}

componentDidUpdate(prevProps) {const { userId } = this.props;
  if (userId ~== prevProps.userId) {getUserApi(userId).then(data => this.setState({ user:data }) 
  }
}
  render() {
 const {user } = this.state;
  return //...
  }
}


첫 번째 렌더링 후에는 componentDidMount 메서드가 호출되고, 두 번째 렌더링부터는 componentDidUpdate 메서드가 호출되므로 API와 통신하는 코드를 양쪽에 작성해야한다.


단, componentDidUpdate메소드에서는 userId가 변경된 경우에만 API를 호출한다. 이처럼 클래스형 컴포넌트에서는 중복된 코드가 여러 생명주기 메소드에 흩어져있는 경우가 많다.

1.3.3 이벤트 처리 함수 등록 및 해제: 함수형 컴포넌트로 작성하기

import React, ( useState, useEffect } from 'react';

function WidthPrinter() {
 const [width, setWidth] = userState(window.innerWidth);
  useEffect( ()=> {
   const onResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', onResize);	❶
   ❷ return () => {
      window.removeEventListener('resize', onResize);
    };
  }, []);return <div>{`width is ${width}`}</div>  
}


창 크기가 변경될 떄마다 onResize함수가 호출되도록 등록한다

useEffect훅의 첫 번째 매개변수에 등록된 함수가 또 다른 함수를 반환할 수 있다.
반환된 함수는 컴포넌트가 언마운트되거나 첫 번째 매개변수로 입력된 함수가 호출되기 직전에 호출된다.
따라서 첫 번째 매개변수로 입력된 함수가 반환한 함수는 프로그램이 비정상적으로 종료되지 않는다면 반드시 호출될 것이 보장된다.

useEffect훅의 두 번째 매개변수에 빈 배열을 넣으면 컴포넌트가 마운트될 때만 첫 번째 매개변수로 입력된 함수가 호출되고,
컴포넌트가 언마운트될때만 반환된 함수가 호출된다.

1.3.4 두 기능을 한 컴포넌트에서 작성하기 : 함수형 컴포넌트로

import React, ( useState, useEffect } from 'react';

function Profile({userID }) {
 const [user, setUser] = useState(null);	
  useEffect (
    ()=> {
      getUserApi(userId).then(data => setUser(data) );
    },
    [userId],
    );

 const [width, setWidth] = userState(window.innerWidth);
  useEffect( ()=> {
   const onResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', onResize);	
    return () => {
      window.removeEventListener('resize', onResize);
    };
  }, []);
  
  return (
    <div>
      {!user && <p>사용자 정보를 가져오는 중...</p>
        { user $$ (
          <>
            <p>{`name is ${user.name}`} </p>
            <p>{ `age is ${user.age}`} </p>
         </>
          )}
    </div>
}

1.4 리액트 내장 훅: useCallback & useReducer


1.4.1 useCallback

리액트의 렌더링 성능을 위해 제공되는 훅이다.
훅을 사용하게 되면 컴포넌트가 렌더링될 때마다 함수를 생성해서 자식 컴포넌트의 속성값으로 입력하는 경우가 많다.
속성값이 매번 변경되기 때문에 자식 컴포넌트에서 불필요한 렌더링이 발생한다는 문제점이 있다.
이 문제점은 useCallback훅으로 해결할 수 있다.

import React, ( useState } from 'react';
import { saveToServer } from './api';
import UserEdit from './UserEdit'

function Profile() {
 const [name, setName] = useState('');
 const [age, setAge] = useState(0);
  return (
    <div>
      <p>{`name is ${user.name}`} </p>
      <p>{ `age is ${user.age}`} </p>
      <UserEdit
        onSave={ ()=> saveToServer(name,age)}
        setName={setName}
        setAge={setAge}
        />
    </div>
    );
}

위 예는 useCallback훅이 필요한 코드다. 그 이유는 Profile컴포넌트가 렌더링될 때마다 UserEdit컴포넌트의 onSave속성값으로 새로운 함수가 입력된다. 따라서 uerEdit컴포넌트에서 불필요한 렌더링이 발생하는데 onSave속성값은 name이나 age값이 변경되지 않으면 항상 같아야 한다.

import React, ( useState } from 'react';
import { saveToServer } from './api';
import UserEdit from './UserEdit'

function Profile() {
 const [name, setName] = useState('');
 const [age, setAge] = useState(0);
 const onSave = useCallback( ()=> saveToServer(name,age), [name,age]);return (
    <div>
      <p>{`name is ${user.name}`} </p>
      <p>{ `age is ${user.age}`} </p>
      <UserEdit
        onSave={onSave}
        setName={setName}
        setAge={setAge}
        />
    </div>
    );
}


이전에는 onSave 속성값으로 전달했던 것과 같은 함수를 useCallback훅의 첫 번째 매개변수로 입력한다.
두 번째 매개변수로 전달한 배열의 값이 변경되지 않으면 이전에 생성한 함수가 재사용된다.
따라서 nameage값이 변경되지 않으면, UserEdit컴포넌트의 onSave 속성값으로 항상 같은 함수가 전달된다

1.4.2 useReducer: 상태값을 리덕스처럼 관리하기

리덕스처럼 관리한다는 것은 여러 상탯값을 하나의 객체로 관리하는 경우에 용이하게 사용가능하다는 뜻이다


import React, { useReducer } from 'react';|	const INITIAL_STATE = { name: 'empty', age: 0 };
|	function reducer(state, action) {
|	 swtich (action.type) {
|   case 'setName':
|   	return { ...state, name: action.name };
|   case 'setAge':
|   	return { ...state, age: action.age };
|   default:
|   	return state;
| 	 }
|	}

function Profile() {
 const [state, dispatch] = useReducer(reducer, INITIAL_STATE);return (
    <div>
      <p>{`name is ${user.name}`} </p>
      <p>{ `age is ${user.age}`} </p>
      <input
        type="text"
        value={state.name}
        onChange={ e=>
          dispatch( { type: 'setName', name: e,.currentTarget.value })}
      />
     <input
        type="number"
        value={state.age}
        onChange={ e=>
          dispatch( { type: 'setAge', age: e,.currentTarget.value })       
        }
      />
   </div>
 )
}


리덕스의 리듀서와 같은 방식으로 작성한 리듀서 함수다.

useReducer훅의 매개변수로 앞에서 작성한 리듀서와 초기 상태값을 입력한다.
useReducer훅은 상태값과 dispatch함수를 차례대로 반환한다.

profile
💻 소프트웨어 엔지니어를 꿈꾸는 개발 신생아👶

2개의 댓글

comment-user-thumbnail
2020년 6월 5일

너무 좋은 글 잘보고 갑니다!! 덕분에 많이 배우고 가요🥳

1개의 답글