React.js Hooks 살펴보기
React.js 에서 Hooks 사용하기
  • Frontend

image

Hooks

Hooks는 React에서 함수형 컴포넌트에서 상태관리를 보다 쉽게 하기위하여 생겨났다.
어떤게 다르게 어떤 종류가 있는지 살펴보고자 한다.

아래 모든 설명에 대한 실습을 해보기 위해서는 react설치가 필요하다.

npx create-react-app react_hooks
cd react_hooks
npm start

useState

useState는 함수형 컴포넌트에서 상태를 관리하는 기본적인 Hook이다.

Hook - useState 실습하기

src디렉토리에 Counter.js를 만들어준다.

useState의 기본 사용법은 const [state, setState] = useState(initState); 으로
값인 state와 값을 대입하는 setState 그리고 초기화값인 initState로 구성되어있다.

Counter.js

import React, { useState } from 'react';

const Counter = () => {
    const [value, setValue] = useState(0);

    return (
        <div>
            <p>현재 값은 : {value}</p>
            <button onClick={() => setValue(value + 1)}>숫자 더하기</button>
            <button onClick={() => setValue(value - 1)}>숫자 빼기</button>
        </div>
    )
};

export default Counter;

그리고 이 Counter가 그려질 수 있도록 App.js를 수정하자

import React from 'react';
import './App.css';
import Counter from './components/Counter';

function App() {
  return (
    <div className="App">
      <Counter />
    </div>
  );
}

export default App;

image

간단하게 숫자를 더하고 빼는 기능이 만들어졌다.

setState라는 함수를 통해 값을 전달하고, 전달받은 값으로 컴포넌트는 리렌더링 하게된다.

React에서는 여러개의 상태를 관리하기 위해서는 여러개의 useState를 사용해야만 한다. 여러개의 Hook을 사용해보자.

Info.js를 만들어보자

import React, { useState } from 'react';

const Info = () => {
    const [name, setName] = useState('');
    const [nickName, setNickName] = useState('');

    return (
        <div>
            <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
            <input type="text" value={nickName} onChange={(e) => setNickName(e.target.value)} />

            <p>이름 : {name}</p>
            <p>닉네임 : {nickName}</p>
        </div>
    )
};

export default Info;

이름과 닉네임을 입력해서 변경되는 간단한 예제이다.

App.js도 Info를 출력하도록 변경해준다.

import React from 'react';
import './App.css';
import Info from './components/Info';

function App() {
  return (
    <div className="App">
      <Info />
    </div>
  );
}

export default App;

결과 : image

상태를 관리해야할 대상이 여러개라도 문제없이 가능하다.

useEffect

useEffect는 컴포넌트가 렌더링 될 때마다 실행되는 함수이다. 렌더링 될 때마다 특정 기능이 실행되야 한다면 이 함수를 사용하면 된다.

useEffect 사용해보기

이전에 만들었던 Info.js에 내용을 추가해보자.

useEffect(() => {
    console.log('렌더링이 완료되었다.');
    console.log(name, nickName);
});

그리고 웹에 들어가서 글자를 입력해보자.

image

글자를 입력할 때마다 콘솔을 열어보면

image

이렇게 출력되는것을 볼 수 있다.

useEffect는 상태가 변경될 때마다 실행되는데, 최초 마운트 되었을 때만 실행하고싶을 때는 두 번째 인자를 빈 배열로 넘겨준다.

useEffect(() => {
    console.log('마운드 될 때만 실행된다.');
}, []);

image

image

글자를 입력하여도 다시 함수가 실행되지 않는다.

useEffect로 특정값이 변경되었을 때만 함수 실행하기

이전에는 어떤 값들이 변경될 때마다.. 혹은 최초 마운트시에만 실행되는것을 살펴보았다.
이번에는 특정 값이 변경될 때만 함수가 실행되도록 처리해보자.

useEffect(() => {
    console.log(name);
}, [name]);

좀 전 빈값으로 넘겼던 배열 안에 name변수를 넣어준다.

image

image

그러면 nickName 변경때는 반응을 하지 않지만, name이 변경되면 함수가 실행되는것을 볼 수 있다.

배열안의 변수는 props로부터 받아온 변수를 넣어도 되고, useState의 변수를 넣어도 된다.

컴포넌트가 마운트 되기 직전, 혹은 렌더링 되기 직전에 실행하고 싶다면..

예를들어 구독과 같은 기능을 개발했다면, 컴포넌트가 언마운트될 때 구독해지를 해야할것이다.

이와같은 경우라면 useEffect의 뒷정리 함수(clean) return을 사용하면 됩니다.

Info.js의 useEffect를 수정한다.

useEffect(() => {
        console.log('mount');

        return () => {
            console.log('unmount!!');
        };
    }, [name]);

그리고, App.js도 수정해준다.

import React, { useState, useEffect } from 'react';
import './App.css';
import Info from './components/Info';

function App() {
  const [visible, setVisible] = useState(true);

  const onSetVisible = () => {
    setVisible(!visible);
  };

  return (
    <div className="App">
      <div>
        { visible ? '보이기' : '숨기기' }
        <button onClick={onSetVisible}>상태 변경하기</button>
      </div>

      { visible && <Info /> }
    </div>
  );
}

export default App;

App.js에서 버튼을 통해 를 보이고 숨기고 하는 간단한 기능이다.

image

image

결과를 확인해보면 버튼을 눌러 컴포넌트가 언마운트될 때 useEffect의 return이 실행된다.

useReducer

useState와 비슷한 useReducer

useReducer는 useState와 비슷하지만 더 다양한 상황에서 사용할 수 있는 Hook이다. 간단한 예제로 살펴보자.
위에서 만들었던 Counter.js에 조금 추가해본다.

import React, { useState, useReducer } from 'react';

const reducer = (state, action) => {
    if (action.type === 'INCREMENT') {
      return { value: state.value + 1 };
    }

    if (action.type === 'DECREMENT') {
        return { value: state.value - 1 };
    }

    return state;
};

const Counter = () => {
    ...
    const [state, dispatch] = useReducer(reducer, { value: 0 } );

    return (
        <div>
            
            ...

            <p>(useReducer) 현재 값은 : {state.value}</p>
            <button onClick={() => dispatch({ type: 'INCREMENT' })}>숫자 더하기</button>
            <button onClick={() => dispatch({ type: 'DECREMENT' })}>숫자 빼기</button>
        </div>
    )
};

export default Counter;

위와같이 useState와 비슷하지만 이번엔 useReducer로 숫자를 더하고, 빼는 기능을 만들었다.

image

결과는 useState와 동일하게 작동한다.
여기서 useState와 다른점을 살펴보면 실제 작동하는 reducer함수가 컴포넌트의 외부에 있다는것을 볼 수 있다.

input의 상태 관리하기

이전에 useState를 사용할 때에는 관리해야할 변수가 많아지면 그만큼 useState의 개수도 많아졌다.
useReducer를 사용하면 좀 더 간편하게 생성할 수 있다.

이전에 만들었던 Info.js와 유사한 페이지를 만들어보자.

import React, { useReducer } from 'react';

const reducer = (state, action) => {
    return {
        ...state,
        [action.name]: action.value,
    }
};

const InfoReducer = () => {
    const [state, dispatch] = useReducer(reducer, {
        name: '',
        nickName: '',
    });

    const onChange = e => {
      dispatch(e.target);
    };

    return (
        <div>
            <input name="name" type="text" value={state.name} onChange={onChange} />
            <input name="nickName" type="text" value={state.nickName} onChange={onChange} />

            <p>이름 : {state.name}</p>
            <p>닉네임 : {state.nickName}</p>
        </div>
    )
};

export default InfoReducer;

input의 name속성을 이용해서 e.target.name을 참조하여 useState해 준 것과 같은 방식으로 동작하는 함수이다.

image

useState를 사용하여 Info.js를 만들었을때와 동일하게 동작한다.
다만 useReducer를 사용하였을 때 input의 개수가 많아지더라도, 간결하고 깔끔한 코드를 유지할 수 있다.

useMemo

useMemo는 함수형 컴포넌트에서 연산을 최적화할 수 있다. 리스트의 숫자들의 평균을 내주는 간단한 예제를 살펴보자.

import React, { useState } from 'react';

const Average = () => {
    const getAverage = list => {
      console.log('getAverage func!!');
      if (!list || !list.length) {
          return 0;
      }

      const sum = list.reduce((sum, item) => sum+item);
      return sum / list.length;
    };

    const [list, setList] = useState([]);
    const [number, setNumber] = useState('');

    const onChange = e => {
        setNumber(e.target.value);
    };

    const addItem = () => {
        setList(list.concat(parseInt(number)));
        setNumber('');
    };

    const listView = list.map(item => (<li key={item}>{item}</li>));

    return (
        <>
          <input type="number" onChange={onChange} />
          <button onClick={addItem}>추가</button>
            <div>
                평균 : {getAverage(list)}
            </div>
            <ul>
                {listView}
            </ul>
        </>
    );
};

export default Average;

image

평균값이 정상적으로 잘 작동한다.

이제 완성했다고 생각할지 모르겠으나, console을 살펴보자.

image

image

input에 숫자를 넣을 때마다 getAverage 함수가 호출되는것을 볼 수 있다. 숫자를 입력할 때에는 함수를 호출할 필요가 없을것이다.
이런경우 useMemo를 사용하여 최적화 할 수 있다.

const avg = useMemo(() => getAverage(list), [list]);

위 useMemo를 만들고 출력을 avg로 해준다.

import React, { useState, useMemo } from 'react';

const Average = () => {
    const getAverage = list => {
        console.log('getAverage func!!');
      if (!list || !list.length) {
          return 0;
      }

      const sum = list.reduce((sum, item) => sum+item);
      return sum / list.length;
    };

    const [list, setList] = useState([]);
    const [number, setNumber] = useState('');

    const onChange = e => {
        setNumber(e.target.value);
    };

    const addItem = () => {
        setList(list.concat(parseInt(number)));
        setNumber('');
    };

    const listView = list.map(item => (<li key={item}>{item}</li>));

    const avg = useMemo(() => getAverage(list), [list]);

    return (
        <>
          <input type="number" value={number} onChange={onChange} />
          <button onClick={addItem}>추가</button>
            <div>
                평균 : {avg}
            </div>
            <ul>
                {listView}
            </ul>
        </>
    );
};

export default Average;

이제 input에 입력할 때에는 getAverage가 호출되지 않는다.

useCallback

useCallback은 useMemo와 비슷한 함수이다. 둘 다 렌더링 성능 최적화에 사용하는 함수로, useMemo는 숫자, 문자열, 객체처럼 일반 값을 재사용할 때 사용하고, 함수를 재사용하려면 useCallback을 사용하면 된다.

위 Average.js에서 onChange와 addItem 함수는 컴포넌트가 리렌더링될 때마다 새로 새성되고 있다. 성능 최적화를 위해 필요한 경우에만 다시 생성되도록 수정해보자.

import React, { useState, useMemo, useCallback } from 'react';

const Average = () => {
    const getAverage = list => {
        console.log('getAverage func!!');
      if (!list || !list.length) {
          return 0;
      }

      const sum = list.reduce((sum, item) => sum+item);
      return sum / list.length;
    };

    const [list, setList] = useState([]);
    const [number, setNumber] = useState('');

    const onChange = useCallback(e => {
        setNumber(e.target.value);
    }, []); // 최초 렌더링시에만 함수를 생성한다.

    const addItem = useCallback(() => {
        setList(list.concat(parseInt(number)));
        setNumber('');
    }, [list, number]); // list 혹은 number가 변경이 있을때에 새로 생성한다.

    const listView = list.map(item => (<li key={item}>{item}</li>));

    const avg = useMemo(() => getAverage(list), [list]);

    return (
        <>
          <input type="number" value={number} onChange={onChange} />
          <button onClick={addItem}>추가</button>
            <div>
                평균 : {avg}
            </div>
            <ul>
                {listView}
            </ul>
        </>
    );
};

export default Average;

성능 최적화를 열심히 해보자.

useRef

useRef를 사용하여 등록버튼 누르면 포커스가 input으로 이동시켜보자.

import React, { useState, useMemo, useCallback, useRef } from 'react';

const Average = () => {
    const inputEl = useRef(null);

    const getAverage = list => {
        console.log('getAverage func!!');
      if (!list || !list.length) {
          return 0;
      }

      const sum = list.reduce((sum, item) => sum+item);
      return sum / list.length;
    };

    const [list, setList] = useState([]);
    const [number, setNumber] = useState('');

    const onChange = useCallback(e => {
        setNumber(e.target.value);
    }, []); // 최초 렌더링시에만 함수를 생성한다.

    const addItem = useCallback(() => {
        setList(list.concat(parseInt(number)));
        setNumber('');
        inputEl.current.focus();
    }, [list, number]); // list 혹은 number가 변경이 있을때에 새로 생성한다.

    const listView = list.map(item => (<li key={item}>{item}</li>));

    const avg = useMemo(() => getAverage(list), [list]);

    return (
        <>
          <input type="number" value={number} onChange={onChange} ref={inputEl} />
          <button onClick={addItem}>추가</button>
            <div>
                평균 : {avg}
            </div>
            <ul>
                {listView}
            </ul>
        </>
    );`
};

export default Average;

input에 useRef변수를 ref로 연결해주면 된다.
.current를 하면 실제 엘리먼트를 가리키고 .current.focus()를 해주면 포커스 이동이 된다.