Java Script/React

React Hooks 알아보기

Zin0_0 2020. 11. 4. 00:20
반응형

Hooks

  • React v16.8에 새로 도입된 기능

  • 함수형 컴포넌트에서도 상태 관리를 할 수 있는 useState, 렌더링 직후 작업을 설정하는 useEffect 등의 기능을 제공

  • 기존의 함수형 컴포넌트에서 할 수 없었던 다양한 작업을 할 수 있게 도움

  • 리액트 매뉴얼에 따르면, 기존의 클래스형 컴포넌트는 앞으로도 계속해서 지원될 예정

  • 하지만, 함수형 컴포넌트와 Hooks 사용 권장 ~> 첫번째 옵션으로 염두하기(미션 요구사항과 부합)

  • 종류

    • useState, useEffect, useReducer, useMemo, useCallback, useRef, customHook
    • useState : 컴포넌트에서 가변적인 상태를 핸들링하도록 돕는 Hook
    • useReducer : 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트할 때 사용
    • useEffect : 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정하는 Hook
    • useMemo : 숫자, 문자열, 객체 등 일반 값을 재사용하기 위한 Hook
    • useCallback : 함수를 재사용하기 위한 Hook
    • useRef : 특정 DOM에 접근 or 렌더링과 관련 없는 변수를 이용할 때 사용하는 Hook(Ref를 쉽게 사용)
  • useState

    • 가장 기본적인 Hook

    • 함수형 컴포넌트에서도 가변적인 상태를 지니도록 도움

      • 함수형 컴포넌트로 상태를 관리한다면, Hook을 이용
    •      import React, { useState } from "react";
      
         const Counter = () => {
             const [value, setValue] = useState(0);
      
             const increaseNum = () => {
                 setValue(value+1);
             };
             const decreaseNum = () => {
                 setValue(value -1 <0 ? value : value-1);
             };
             return (
                 <div>
                     <h1>Increasing : {value}</h1>
                     <button onClick={() => increaseNum()}>
                     +1 시키기
                     </button>
                     <button onClick={() => decreaseNum()}>
                     -1 시키기
                     </button>
                 </div>
             );
      
         } 
         export default Counter;
      • useState(0) => value의 기본 값을 0으로 설정하겠다는 의미
      • 이 함수가 호출되면 배열을 반환 ~> 첫 번째 원소 : 상태 값, 두 번째 원소 : 상태 설정 함수
    • useState 함수는 하나의 상태 값만 관리함 ~> 컴포넌트가 여러 상태를 관리 한다면 ~> useState를 여러 번 이용

      •       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 value={name} onChange={onChangeName} />
                            <input value={nickName} onChange={onChangeNickName} />
                        </div>
                        <div>
                            <b>이름:</b> {name}
                        </div>
                        <div>
                            <b>닉네임:</b> {nickName}
                        </div>
                    </div>
                );
            };
        
            export default Info;
  • useEffect

    • 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정하는 Hook

    • useEffect => 클래스 컴포넌트의 componentDidMount + componentDidUpdate (moust와 update)

    •       import React, { useEffect, useState } from 'react';
      
          const Info = () => {
              const [name, setName] = useState('');
              const [nickName, setNickName] = useState('');
      
              useEffect(() => {
                  console.log('렌더링 완료');
                  console.log({name, nickName});
              });
      
              const onChangeName = (e) => {
                  setName(e.target.value);
              }
              const onChangeNickName = (e) => {
                  setNickName(e.target.value);
              }
      
              return (
                  <div>
                      <div>
                          <input value={name} onChange={onChangeName} />
                          <input value={nickName} onChange={onChangeNickName} />
                      </div>
                      <div>
                          <b>이름:</b> {name}
                      </div>
                      <div>
                          <b>닉네임:</b> {nickName}
                      </div>
                  </div>
              );
          };
      
          export default Info;
      • input에 입력이 일어날 때마다 name 값과 nickName값이 바뀌기 때문에, 계속 렌더링 작업이 일어남 ~> console에 계속 찍힌다.
      • image
    • 마운트 될 때만 실행하기

      • 마운트 = 컴포넌트가 맨 처음 렌더링될 때만 실행하고, 업데이트 될 때는 실행하지 않는 것
      • useEffect함수의 두 번째 파라미터에 비어 있는 배열을 넣으면 됨
      •       useEffect(() => {
                console.log('마운트될 때만 실행됩니다.');
            }, []);
      • image
    • 업데이트될 때만 실행하기

      • 클래스형 컴포넌트
        •       componentDidUpdate(prevProps, prevState) {
                  if(prevProps.value !== this.props.value) {
                      doSomething();
                  }
              }
        • props 안에 있는 value 값이 바뀔 때만 작업을 수행하도록 함
      • 함수형 컴포넌트
        •       useEffect(() => {
                  console.log(name);
              }, [name]);
        • 위와 같이, 두 번째 파라미터의 배열 안에 업데이트를 검사하고 싶은 값을 넣어주면 됨
        • image
    • 뒷정리하기

      • useEffect는 렌더링되고 난 직후마다 실행되고, 두 번째 파라미터 배열의 값에 따라 조건이 달라진다.
      • 컴포넌트가 unMount되기 전이나 update되기 직전에 특정 작업을 수행하고 싶다면, useEffect에서 뒷정리(cleanup) 함수를 반환해주어야 한다.
      •       useEffect(() => {
                console.log('effect');
                console.log(name);
                return () => {
                  console.log('cleanup');
                  console.log(name);
                };
            });
      • image
      • 최초 렌더링 될 때, Effect가 적용되고 다음 useEffect가 실행되기 전에 cleanup 함수를 실행
      • 더 명확하게 확인하기 위해, 버튼을 누르면 input과 이름 보이고 사라지게 설정해보자
      •       <div>
                <button onClick={() => {
                        setVisible(!visible);
                    }}>
                    {visible ? '숨기기' : '보이기'}
                </button>
                <hr />
                {visible && <Info />}
            </div>
      • image
      • 최초 보이기 버튼을 누르면 useEffect 실행 ~> effect 호출, 숨기기 버튼 ~> cleanup 실행, 다시 보이기 버튼 누르면 useEffect 실행
    • 언마운트될 때만 뒷정리 함수를 호출해보자

      • 언마운트될 때만 cleanup 함수를 호출하려면, useEffect 함수의 두 번째 파라미터에 빈 배열 전달
      • 마운트 될 때와 파라미터가 같음
      •       useEffect(() => {
                console.log('effect');
                console.log(name);
                return () => {
                    console.log('cleanup');
                    console.log(name);
                };
            }, []);
  • useReducer

    • useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트할 때 사용

    • 리듀서 ~~> 현재 상태, 업데이트에 필요한 정보를 담은 액션(action) 값을 전달받아 새로운 상태 반환

    • 리듀서 함수에서 새로운 상태를 만들 때는 반드시 불변성을 지켜야함

    •       import React, { useReducer } from "react";
      
          const reducer = (state, action) => {
              switch(action.type) {
                  case 'INCREASEMENT':
                      return { value:state.value+1 };
                  case 'DECREASEMENT':
                      return { value:state.value-1 };
                  default:
                      return state; // 기존 상태
              }
          }
      
          const Counter = () => {
              const [state, dispatch] = useReducer(reducer, { value:0 });
      
              return (
                  <div>
                      <h1>Increasing : {state.value}</h1>
                      <button onClick={() => dispatch({type: 'INCREASEMENT'})}>
                      +1 시키기
                      </button>
                      <button onClick={() => dispatch({type: 'DECREASEMENT'})}>
                      -1 시키기
                      </button>
                  </div>
              );
          } 
      
          export default Counter;
    • useReducer(리듀서 함수, 리듀서 기본 값)으로 사용

    • 이 함수를 사용하면 state 값과 dispatch 함수를 받아온다.

    • state는 현재 상태, dispatch는 액션을 발생시키는 함수

    • 장점

      • 컴포넌트 업데이트 로직을 컴포난트 밖으로 뺄 수 있음
    • 인풋 상태 관리하기

      • 이전 useState를 여러번 사용했던 예제에 useReducer를 적용해보자.

      •       import React, { useReducer } from 'react';
        
            const 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 name ="name" value={name} onChange={onChange} />
                            <input name ="nickName" value={nickName} onChange={onChange} />
                        </div>
                        <div>
                            <b>이름:</b> {name}
                        </div>
                        <div>
                            <b>닉네임:</b> {nickName}
                        </div>
                    </div>
                );
            };
        
            export default Info;
      • 관리해야하는 상태가 많아질 때, 훨씬 효율적인 코드라고 느낌

  • useMemo

    • 함수형 컴포넌트 내부에서 발생하는 연산 최적화에 사용

    •      import React, { useState, useMemo } from 'react';
      
         const getAverage = numbers => {
           console.log('평균값 계산 중..');
           if (numbers.length === 0) return 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 = () => {
             const nextList = list.concat(parseInt(number));
             setList(nextList);
             setNumber('');
           };
      
           const avg = useMemo(() => getAverage(list), [list]);
      
           return (
             <div>
               <input value={number} onChange={onChange} />
               <button onClick={onInsert}>등록</button>
               <ul>
                 {list.map((value, index) => (
                   <li key={index}>{value}</li>
                 ))}
               </ul>
               <div>
                 <b>평균값:</b> {avg}
               </div>
             </div>
           );
         };
      
         export default Average;
      • 평균값에서 getAverage()를 계속 호출하면, 렌더링 할때마다 계산이 새롭게돼서 시간이 낭비됨 (값이 많을수록, 그리고 input에 무언가를 계속 입력할수록)
      • useMemo를 이용해서 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하도록 설정
      • 의존성이 변경되었을 때만(배열로 전달한 값이 변경되었을 때만) 메모이제이션된 값만 다시 계산해준다.(반환한다)
      •     const colorMemo = useMemo(() => getColor(color), [color]);
          const movieGenreMemo = useMemo(() => getMovieGenre(movie), [movie]) ;
      • 이 경우는, color값이 변경되면 getColor 함수만, movie값이 변경되면 getMovie함수만 호출된다.
  • useCallback

    • useMemo와 비슷함

    • 렌더링 성능을 최적화 해야하는 상황에서 사용 ~> 이벤트 핸들러 함수를 필요한 상황에서만 생성

    • 컴포넌트의 렌더링이 자주 발생하거나, 렌더링할 컴포넌트 개수가 많아지면 최적화

    •       import React, { useState, useMemo, useCallback } from 'react';
      
          const getAverage = numbers => {
            console.log('평균값 계산 중..');
            if (numbers.length === 0) return 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 = useCallback(e => {
              setNumber(e.target.value);
            },[]); // 컴포넌트가 처음 렌더링될 때만 생성
      
            const onInsert = useCallback(() => {
              const nextList = list.concat(parseInt(number));
              setList(nextList);
              setNumber('');
            }, [number, list]); // number of list가 바뀔 때만 생성
      
            const avg = useMemo(() => getAverage(list), [list]);
      
            return (
              <div>
                <input value={number} onChange={onChange} />
                <button onClick={onInsert}>등록</button>
                <ul>
                  {list.map((value, index) => (
                    <li key={index}>{value}</li>
                  ))}
                </ul>
                <div>
                  <b>평균값:</b> {avg}
                </div>
              </div>
            );
          };
      
          export default Average;
      • 렌더링이 될 때마다 함수를 생성하는 것 (재선언됨) 은 비효율적이기 때문에, 마운트 될 때만 선언하고 재사용하기 위해 등장
      • useCallback의 첫 번째 파라미터 ~> 생성하고 싶은 함수, 두 번째 파라미터 ~> 배열
      • 배열에 어떤 값이 바뀌었을 때 함수를 생성해야 하는지 명시 (위에서 number, list, 첫 렌더링)
      • 의존성이 변경될 때(두 번째 파라메터인 배열 값이 변경될 때) 메모이제이션된 콜백 함수가 재선언된다.
      • shouldComponentUpdate를 사용하여) 참조의 동일성에 의존적인 최적화된 자식 컴포넌트에 콜백을 전달할 때 유용
      • This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
      • useCallback은 useMemo로 함수를 반환하는 상황에서 더 편리하게 이용하는 Hook
      • 숫자, 문자열, 객체 등 일반 값을 재사용 ~> useMemo, 함수 재사용 ~> useCallback
  • useRef

    • 함수형 컴포넌트에서 ref를 쉽게 사용하도록 도움

    • 위에서 작성한 Average 컴포넌트에서 등록 버튼을 눌렀을 때, 포커스가 인풋으로 넘어가게 작성

    •       import React, { useState, useMemo, useCallback, useRef } from 'react';
      
          const getAverage = numbers => {
            console.log('평균값 계산 중..');
            if (numbers.length === 0) return 0;
            const sum = numbers.reduce((a, b) => a + b);
            return sum / numbers.length;
          };
      
          const Average = () => {
            const [list, setList] = useState([]);
            const [number, setNumber] = useState('');
            const inputEl = useRef(null);    // useRef 추가
      
            const onChange = useCallback(e => {
              setNumber(e.target.value);
            },[]); // 컴포넌트가 처음 렌더링될 때만 생성
      
            const onInsert = useCallback(() => {
              const nextList = list.concat(parseInt(number));
              setList(nextList);
              setNumber('');
              inputEl.current.focus();    // input태그를 가리키도록
            }, [number, list]); // number of list가 바뀔 때만 생성
      
            const avg = useMemo(() => getAverage(list), [list]);
      
            return (
              <div>
                <input value={number} onChange={onChange} ref={inputEl}/>
                <button onClick={onInsert}>등록</button>
                <ul>
                  {list.map((value, index) => (
                    <li key={index}>{value}</li>
                  ))}
                </ul>
                <div>
                  <b>평균값:</b> {avg}
                </div>
              </div>
            );
          };
      
          export default Average;
    • 로컬 변수 사용하기

      • 컴포넌트 로컬 변수 사용 시, useRef를 활용

      • 로컬 변수 ~> 렌더링과 상관 없이 바뀔 수 있는 값

      •       import React, { useRef } from 'react';
        
            const RefSample = () => {
              const id = useRef(1);
              const setId = (n) => {
                id.current = n;
              }
              const printId = () => {
                console.log(id.current);
              }
              return (
                <div>
                  refsample
                </div>
              );
            };
        
            export default RefSample;
        • ref 안의 값이 바뀌어도 컴포넌트가 렌더링되지 않음 (렌더링과 관계 없는 변수)
  • 커스텀 Hooks 만들기

    • 여러 컴포넌트에서 비슷한 기능을 공유할 때, 이를 custom Hook으로 작성해서 재사용하는 목적

    • 앞서 작성했던 예시에서 useReducer로 작성했던 input 로직을 useInputs라는 Hook으로 분리하기

    •       // useInputs.js
          import { useReducer } from "react";
      
          const reducer = (state, action) => {
              return {
                  ...state,
                  [action.name]: action.value
              }
          }
      
          const useInputs = initialForm => {
              const [state, dispatch] = useReducer(reducer, initialForm);
              const onChange = e => {
                  dispatch(e.target);
              }
              return [state, onChange];
          };
      
          export default useInputs;
    •       import React from 'react';
          import useInputs from './useInputs';
      
          const Info = () => {
              const [state, onChange] = useInputs({
                  name: '',
                  nickName: ''
              });
              const {name, nickName} = state;
      
              return (
                  <div>
                      <div>
                          <input name ="name" value={name} onChange={onChange} />
                          <input name ="nickName" value={nickName} onChange={onChange} />
                      </div>
                      <div>
                          <b>이름:</b> {name}
                      </div>
                      <div>
                          <b>닉네임:</b> {nickName}
                      </div>
                  </div>
              );
          };
      
          export default Info;
      • input 태그 안에서 state를 관리할 때, useInputs를 활용하면 다른 곳에서 재사용이 가능

Reference

리액트를 다루는 기술

반응형