Hooks

Posted by yunki kim on June 29, 2021

  hooks는 리액트 v16.8에서 도입된 기능으로 함수형 컴포넌트에서 상태 관리를 할 수 있는 useStat, 렌더링 직후 작업을 설정하는 useEffect등의 기능을 제공한다.

useState

  useState는 hook의 가장 기본적인 hook으로서 함수형 컴포넌트가 가변적인 상태를 가지게 해준다. 하나의 useState는 하나의 상태만 관리할 수 있다.

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
import React, {useState} from 'react';
 
const Info = () => {
    const [name, setName] = useState('');
    const [nickname, setNickname] = useState('');
 
    const onChangeName = (e) => {
        setName(e.target.value);
    }
 
    const onChangeNickName = (e) => {
        setNickname(e.target.value);
    }
 
    return(
        <div>
            <div>
                <input type="text" placeholder={"name"} onChange={onChangeName}/>
                <input type="text" placeholder={"nickname"} onChange={onChangeNickName}/>
            </div>
            <div>
                <div>name: {name}</div>
                <div>nickname: {nickname}</div>
            </div>
        </div>
    )
}
 
export default Info;
cs

useEffect

  리액트 컴포넌트가 렌더링 될때 마다 특정 작업을 수행하게 한다.  클래스형 컴포넌트의 componentDidMount, componentDidUpdate를 합친 형태로 보아도 무방하다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//위 코드에서:
import React, {useState, useEffect} from 'react';
 
const Info = () => {
       //...
    useEffect(() => {
        console.log('rendering complete');
        console.log({
            name,
            nickname
        });
    });
    //...
}
 
export default Info;
cs

 

  만약 useEffect를 컴포넌트가 맨 처음에 렌더링 될때만 실행하고, 업데이트 될때는 실행하게 하고 싶지 않다면 함수의 두번쨰 파라미터로 비어있는 배열을 넣으면 된다.

1
2
3
4
5
6
7
 useEffect(() => {
    console.log('rendering complete');
    console.log({
        name,
        nickname
    });
}, []);
cs

 

  특정 값이 바뀔때만 useEffect를 사용하고 싶다면 두번째 인자인 배열 안에 그 값을 넣어주면 된다

1
2
3
4
5
6
7
 useEffect(() => {
    console.log('rendering complete');
    console.log({
        name,
        nickname
    });
}, [name]);
cs

언마운트 이전, 업데이트 직전의 작업

  useEffect는 기본적으로 렌더링이 끝난 직후 수행이 된다. 또 한 두번째 파라피터로 어떤 값을 줄것이냐에 따라 실행되는 조건이 달라 진다. 만약 언마운트 이전, 업데이트 직전에 어떤 작업을 하고 싶다면 useEffect에서 cleanup 함수를 반환하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
useEffect(() => {
    console.log('rendering complete');
    console.log({
        name,
        nickname
    });
    //cleanup function
    return () => {
        console.log('cleanup');
        console.log(name);
    };
}, [name]);
cs

 

 

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
//App.js
import logo from './logo.svg';
import React, {useState} from 'react';
import './App.css';
import Info from "./Info";
 
function App() {
  const [visible, setVisible] = useState(false);
  return (
      <div>
        <button
            onClick={()  => {
              setVisible(!visible);
            }}
        >
          {visible ? '숨기기' : '보이기'}
        </button>
        <hr/>
        {visible && <Info/>}
      </div>
  );
}
 
export default App;
 
//Info.js
import React, {useState, useEffect} from 'react';
 
const Info = () => {
    const [name, setName] = useState('');
    const [nickname, setNickname] = useState('');
 
    useEffect(() => {
        console.log('rendering complete');
        console.log(name);
        return () => {
            console.log('cleanup');
            console.log(name);
        };
    }, [name]);
 
    const onChangeName = (e) => {
        setName(e.target.value);
    }
 
    const onChangeNickName = (e) => {
        setNickname(e.target.value);
    }
 
    return(
        <div>
            <div>
                <input type="text" placeholder={"name"} onChange={onChangeName}/>
                <input type="text" placeholder={"nickname"} onChange={onChangeNickName}/>
            </div>
            <div>
                <div>name: {name}</div>
                <div>nickname: {nickname}</div>
            </div>
        </div>
    )
}
 
export default Info;
cs

위 예제의 결과를 보면 useEffect의 rendering complete부분은 현재 바뀐 상태를, cleanup 함수는 바뀌기 한 단계 이전 값을 보여주는 것을 알 수 있다.

언마운트 될때만 호출하기

  cleanup함수를 넣고 useEffect의 두번째 파라미터로 빈 배열을 넣으면 된다.

1
2
3
4
5
6
7
8
 useEffect(() => {
    console.log('rendering complete');
    console.log(name);
    return () => {
        console.log('cleanup');
        console.log(name);
    };
}, []);
cs

useReducer

  useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다름 값으로 업데이트를 해주고 싶을 때 사용한다. reducer는 현재 상태 그리고 업데이트를 위한 정보를 담은 action값을 전달받아 새로운 상태를 반환하는 함수이다. reducer함수를 만들떄는 불변성을 지켜주어야 한다. 따라서 반드시 업데이트 한 상태를 반환해야 한다.

  액션 값은 주로 다음과 같은 형태로 이루어져 있지만 액션 객체가 반드시 타입을 가져야 하는 것은 아니며 객체가 아닌 문자열, 숫자여도 상관 없다.

1
2
3
{
    type: 'INCREMENT',
}
cs

 

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
import React, {useReducer} from 'react';
 
function reducer(state, action) {
    switch (action.type) {
        case 'INCREMENT':
            //불변성 유지
            return {value: state.value + 1};
        case 'DECREMENT':
            return {value: state.value - 1};
        default:
            return state;
    }
}
 
 
const Counter = () => {
    const [state, dispatch] = useReducer(reducer, {value: 0});
    return (
        <div>
            <p>
                현재 카운터 값은 {state.value}
            </p>
            <button onClick={() => dispatch({type: 'INCREMENT'})}>+1</button>
            <button onClick={() => dispatch({type: 'DECREMENT'})}>-1</button>
        </div>
    )
}
 
export default Counter;
cs

 

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
import React, {useReducer} from 'react';
 
function reducer(state, action) {
    return {
        ...state,
        [action.name]: action.value,
    };
}
 
const Info = () => {
    const [state, dispatch] = useReducer(reducer, {
        name'',
        nickname: '',
    });
 
    const {name, nickname} = state;
    const onChange = (e) => {
        //액션은 어떤 값이여도 상관 없다.
        dispatch(e.target);
    };
 
    return (
        <div>
            <div>
                <input type="text"
                       name={"name"}
                       value={name}
                       onChange={onChange}
                />
                <input type="text"
                       name={"nickname"}
                       value={nickname}
                       onChange={onChange}
                />
            </div>
            <div>
                <div>이름: {name}</div>
            </div>
            <div>닉네임: {nickname}</div>
        </div>
    );
};
 
export default Info;
cs

useMemo

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
import React, {useState} from 'react';
 
const getAverage = (numbers) => {
    console.log('calculating average');
    if(numbers.length === 0return 0;
    const sum = numbers.reduce((a, b) => a + b);
    return sum / numbers.length;
}
 
const Average = () => {
    const [list, setList] = useState([]);
    const [number, setNumber] = useState('');
 
    const onChange = (e) => {
        setNumber(e.target.value);
    };
 
    const onInsert = (e) => {
        const nextList = list.concat(parseInt(number));
        setList(nextList);
        setNumber('');
    };
 
    return (
        <div>
            <input type="text" value={number} onChange={onChange}/>
            <button onClick={onInsert}>confirm</button>
            <ul>
                {list.map((value, index) => (
                    <li key={index}>{value}</li>
                ))}
            </ul>
            <div>
                average: {getAverage(list)}
            </div>
        </div>
    );
}
 
export default Average;
cs

위와 같은 코드를 실행시키면 input에 숫자를 넣을때도 getAverage 함수가 호출된다. 이러한 낭비를 줄일려면 useMemo를 사용하면 된다. useMemo를 사용하면 렌더링 과정에서 특정 값이 바뀌었을 때만 연산을 수행할 수 있다.

위 코드에서:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, {useState, useMemo} from 'react';
 
const getAverage = (numbers) => {
    //...
}
 
const Average = () => {
    //...
    const avg = useMemo(() => getAverage(list), [list]);
 
    return (
        <div>
           //...
            <div>
                average: {avg}
            </div>
        </div>
    );
}
 
export default Average;
cs

  그런데, 여기서 질문이 하나 생긴다. 위의 코드에서 useMemo를 사용하는 대신에 useEffect를 사용해도 되지 않을까? 

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
import React, {useState, useEffect} from 'react';
 
const getAverage = (numbers) => {
    console.log('calculating average');
    if(numbers.length === 0return 0;
    const sum = numbers.reduce((a, b) => a + b);
    return sum / numbers.length;
}
 
const Average = () => {
    const [list, setList] = useState([]);
    const [number, setNumber] = useState('');
    const [avgNum, setAvgNum] = useState('');
 
    const onChange = (e) => {
        setNumber(e.target.value);
    };
 
    const onInsert = (e) => {
        const nextList = list.concat(parseInt(number));
        setList(nextList);
        setNumber('');
    };
 
    useEffect(() => {
        if(list.length === 0return;
        const sum = list.reduce((a, b) => a + b);
        setAvgNum(sum / list.length);
    }, [list]);
 
    return (
        <div>
            <input type="text" value={number} onChange={onChange}/>
            <button onClick={onInsert}>confirm</button>
            <ul>
                {list.map((value, index) => (
                    <li key={index}>{value}</li>
                ))}
            </ul>
            <div>
                average: {avgNum}
            </div>
        </div>
    );
}
 
export default Average;
cs

  위 코드에서 볼 수 있듯이 useEffect를 사용해도 아주 잘 돌아 간다. 그러면, useEffect로도 해결 가능한 일을 useMemo가 한다는 의미인대 대체 왜 굳이 useMemo라는 것이 존재할까? 

  이에 대한 답변은 다음과 같다.

    useEffect는 side effect를 위한 것이고, useMemo는 no side-effects를 위한 것이다. 리엑트 공식문서를 보면 알겠지만 useEffect에 대한 설명을 할때 사이드 이펙트에 대한 설명이 등장하며 useEffect는 렌더링이 끝난 다음에 발동되기 때문에 side effect로 부터 안전하다. 또 한 useMemo는 메모이제이션 정도의 기능만을 제공하기 위한 것 이므로 애플리케이션의 동작의 변경이 아닌 오직 퍼포먼스 향상을 위해서만 사용해야 한다.

  side effect

    side effect는 현재 실행되는 함수의 스코프 밖에 있는 무언가에 대해 영향을 주는 것을 의미한다

      ex). API requests to backend, Calls to out authenticatio service.

  useMemo의 주의 사항.

    코드 최적화는 아주 중요한 문제이다. 위 예제에서 useMemo를 사용하면 필요없는 자원낭비를 줄일 수 있다고 했다. 하지만 무조건 적인 useMemo의 사용이 이득이 되는것은 아니다. useMemo는 메모이제이션을 사용하기 때문이다. 따라서 다음과 같은 질문을 해보아야 한다.

      1. useMemo를 사용하고자 하는 함수의 비용이 얼마인가. 대부분의 js 데이터 타입들에 대한 메서드들 (Array.map등)은 이미 최적화가 되어 있기 때문에 연산에 대한 비용이 높지 않다. 따라서 굳이 메모이제이션을 하지 않아도 된다. 만약 useMemo를 남용한다면 useMemo의 비용이 더 높은 상황이 발생할 수 있다.

      2. 데이터가 primitive type인가? 

1
2
3
4
5
6
7
8
/** 
  @param {number} page 
  @param {string} type 
**/
const MyComponent({page, type}) {
  const resolvedValue = getResolvedValue(page, type)
  return <ExpensiveComponent resolvedValue={resolvedValue}/> 
}
cs

위와 같은 코드가 있다고 해보자. resolvedValue가 primitive type이라면 레퍼런스는 바뀌지 않는다. 따라서 ExpensiveComponent는 리렌더되지 않는다.

 

useCallback 

  useMemo와 상당히 비슷하다. 렌더링 성능 최적화를 위해 사용하며 만들어놓았던 함수를 재사용할 수 있다. 위의 평균값을 구하는 예제는 컴포넌트가 리렌더링 될때마다 새로 만들어진 함수를 사용한다. 따라서 리렌더링이 자주 일어나거나 컴포넌트 개수가 많아 지면 이 부분을 최적화 해야 한다. 

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
import React, {useState, useMemo, useCallback} from 'react';
 
const getAverage = (numbers) => {
    //...
}
 
const Average = () => {
  //...
    const onChange = useCallback((e) => {
        setNumber(e.target.value);
    }, []);//컴포넌트가 처음 렌더링 될때만 함수 생성
 
    const onInsert = useCallback((e) => {
        const nextList = list.concat(parseInt(number));
        setList(nextList);
        setNumber('');
    }, [number, list]);
 
    return (
       //...
    );
}
 
export default Average;
 
cs

 

useRef

  함수형 컴포넌트에서 ref를 쉽게 사용할 수 있게 한다. 위 코드에서 버튼을 눌렀을때 포커스가 인풋으로 넘어가게 작성해 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, {useState, useMemo, useCallback, useRef} from 'react';
 
const getAverage = (numbers) => {
    //..
}
 
const Average = () => {
   //..
    const inputE1 = useRef(null);
 
   //..
 
    return (
        <div>
            <input type="text" value={number} onChange={onChange} ref={inputE1}/>
           //..
        </div>
    );
}
 
export default Average;
 
cs

 

커스텀 훅

  여러 컴포넌트가 비슷한 기능을 공유 할때 커스텀훅을 사용해 로직을 재사용할 수 있다.

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
//useInputs.js
//custom hook
import {useReducer} from 'react';
 
function reducer(state, action) {
    return {
        ...state,
        [action.name]: action.value
    };
}
 
export default function useInputs(initialFrom) {
    const [state, dispatch] = useReducer(reducer, initialFrom);
    const onChange = e => {
        dispatch(e.target);
    };
    return [state, onChange];
}
 
//Info.js
import React from 'react';
import useInputs from "./useInputs";//커스텀 훅을 가져온다
 
const Info = () => {
    const [state, onChange] = useInputs({
        name'',
        nickname: '',
    });
    const {name, nickname} = state;
 
    return (
        <div>
            <div>
                <input type="text"
                       name={"name"}
                       value={name}
                       onChange={onChange}
                />
                <input type="text"
                       name={"nickname"}
                       value={nickname}
                       onChange={onChange}
                />
            </div>
            <div>
                <div>이름: {name}</div>
            </div>
            <div>닉네임: {nickname}</div>
        </div>
    );
};
 
export default Info;
cs