React.js - 리액트 훅에 대해 알아보자

Gyu·2022년 5월 2일
3

React.js

목록 보기
10/20
post-thumbnail

Hook이란?

  • Hook은 리액트 v16.8에 새로 도입된 기능으로 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수다. Hook은 class 안에서 동작하지 않으며, class를 사용하지 않고 React를 사용할 수 있게 해준다.

useState

import React, { useState } from 'react'; // import 구문을 통해 불러오기

const [value, setValue] = useState('초기값'); 
// 비구조화 할당으로 useState() 함수의 리턴값을 변수에 저장 
  • 가장 기본적인 Hook이며 함수형 컴포넌트에서도 가변적인 state를 지닐 수 있게 해준다. 클래스 컴포넌트의 this.state가 제공하는 기능과 똑같다. 일반적으로 일반 변수는 함수가 끝날 때 사라지지만, state 변수는 React에 의해 사라지지 않는다. 변수 명은 개발자가 원하는대로 지어도 된다.
  • state의 초깃값을 매개변수로 전달받아 배열을 반환한다. 클래스 컴포넌트에서 state 초깃값은 객체여야 하지만, useState() 에서 초깃값의 형태는 자유다.
  • useState() 는 길이가 2인 배열을 반환한다. 배열의 첫 번째 원소는 현재 상태이고, 두 번째 원소는 상태를 설정하는 함수(setter)다.
  • useState() 예제
    import React, { useState } from 'react';
    
    const Info = () => {
    	// useState()를 여러개 사용할 경우
        const [ name, setName ] = useState('');
        const [ number, setNumber ] = useState('');
    
        const changeName = e => {
            setName(e.target.value);
        }
    
        const changeNumber = e => {
            setNumber(e.target.value);
        }
        return (
            <div>
                <input value={name} onChange={changeName}/>
                <input value={number} onChange={changeNumber}/>
                <h1>{name}</h1>
                <h1>{number}</h1>
            </div>
        );
    };
    
    export default Info;

useEffect

import React, { useEffect } from 'react';

useEffect(effects[, deps]);
  • 컴포넌트가 렌더링 될 때 마다 특정 작업을 수행하도록 설정하는 Hook. useEffect() 는 함수 컴포넌트 내에서 이런 side effects를 수행할 수 있게 해준다. class 컴포넌트의 componentDidMountcomponentDidUpdate, componentWillUnmount와 같은 목적으로 제공되지만, 하나의 API로 통합된 것이다.

💡 Side Effect란?

일반적으로 side effect는 부작용, 의도하지 않는 결과를 의미한다. 컴퓨터 공학에서의 side effect란 함수의 로컬 상태를 함수 외부에서 변경하는 것을 말한다. 함수를 입력값에 대해 일정한 출력을 하는 것으로 가정할 때, 출력값에 영향을 미치지 않는 모든 작업들, 예로들어 로그찍기, 네트워크 통신, API 호출 등을 side effect라고 할 수 있다.

이 개념을 react에 적용하면 컴포넌트 안에서 데이터를 가져오거나 구독하고, DOM을 직접 조작하는 작업을 “side effects”(또는 짧게 “effects”)라고 할 수 있다. 왜냐하면 이러한 작업들은 다른 컴포넌트에 영향을 줄 수도 있고, 렌더링 과정에서는 구현할 수 없는 작업이기 때문이다.

  • useEffect() 는 effects 함수를 첫 번째 매개변수로 전달받아, 컴포넌트가 렌더링 될 때마다 effects 함수를 실행한다.
  • useState()와 같이 중복 사용이 가능하다.
  • useEffect()는 두 번째 매개변수로 배열을 받는다. 이 배열에는 useEffect() 함수가 변화를 감지할 state를 요소로 넣는다.
  • 빈배열을 매개변수로 넣으면 컴포넌트가 화면에 맨 처음 렌더링 될 때만 effects 함수가 실행 된다.
  • 특정 state를 배열의 요소에 넣으면, 해당 state가 변경될 때만 effects 함수가 실행된다.

뒷정리(clean-up)

  • useEffect()는 기본적으로 렌더링되고 난 직후마다 실행되며, 두 번째 매개변수 배열에 무엇을 넣는지에 따라 실행되는 조건이 달라진다. 컴포넌트가 언마운트 되기 전이나 업데이트 직전에 어떠한 작업을 수행하고 싶다면 useEffect()함수에서 뒷정리(clean-up) 함수를 반환해야 한다.
  • 뒷정리 함수는 컴포넌트가 마운트 되기 전과 업데이트 전에 실행되며, 업데이트 전에 실행될 경우 업데이트 직전 state 값에 접근할 수 있다.
  • 뒷정리 함수는 보통 데이터 구독 취소 등 뒷정리가 필요한 경우에만 사용한다.
  • useEffect() 사용 예
    import React, { useState, useEffect } from 'react';
    
    const Info = () => {
        const [ name, setName ] = useState('');
    
        const changeName = e => {
            setName(e.target.value);
        }
    
        useEffect(() => {
            console.log('effct!');
            console.log(`name : ${name}`);
            
            return () => { // 언마운트전, 업데이트전만 실행
                console.log('clean');
                console.log(`name : ${name}`); // 업데이트 직전 값에 접근
            }
        }, [name]) // name이 변경될 때만 effects 함수 실행
    
        return (
            <div>
                <input value={name} onChange={changeName}/>
                <h1>{name}</h1>
            </div>
        );
    };
    
    export default Info;

useEffect 사용 시기

  • 렌더링 시(마운트 될 때, 업데이트 될 때) 특정 작업하고 싶을 때 사용
  • 언 마운트 전, 업데이트 전 특정 작업을 하고 싶을 때는 뒷정리 함수 사용
  • 주로 마운트 시에 하는 작업
    • props 로 받은 값을 컴포넌트의 로컬 상태로 설정
    • 외부 API 요청 (REST API 등)
    • 라이브러리 사용 (D3, Video.js 등...)
    • setInterval 을 통한 반복작업 혹은 setTimeout 을 통한 작업 예약
  • 주로 언마운트 시 하는 작업
    • setInterval, setTimeout 을 사용하여 등록한 작업들 clear 하기 (clearInterval, clearTimeout)
    • 라이브러리 인스턴스 제거

useReduce

import React, { useReducer } from 'react';

const [state, dispatch] = useReducer(reducer, initialArg);
  • useState()의 확장형 대체 함수로, 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트 할 때 주로 사용하는 Hook.
  • 다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우나 다음 state가 이전 state에 의존적인 경우, state를 다루는 경우 등 주로 사용한다.
  • reducer 함수와 초기값(initialArg)을 전달받아 상태(state)와 액션을 발생시키는 함수(dispatch)가 담긴 배열을 반환한다.

reducer

(state, action) => newState
  • useReducer() 함수의 첫 번째 매개변수. 현재 상태(state)와 업데이트를 위해 필요한 정보(action)을 매개변수로 전달받아 새로운 상태를 반환하는 함수.
  • action의 type은 상관없으며, reducer 함수는 컴포넌트 밖에서 선언이 가능하다.

dispatch

dispatch(action);
  • 액션값을 매개변수로 전달받아 reducer 함수를 호출 기능의 함수.

useReducer 예제

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

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

export default Counter;

여러 state를 다루는 useReducer 예

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

function reducer (state, action) {
    return {
        ...state,
        [action.name]: action.value
    }
}

const Info = () => {
    const [state, dispatch] = useReducer(reducer, {
        name: '',
        number: ''
    })

    const changeValue = (e) => {
        dispatch(e.target)
    }

    return (
        <div>
            <input name="name" value={state.name} onChange={changeValue}/>
            <input name="number" value={state.number} onChange={changeValue}/>
            <h1>{state.name}</h1>
            <h1>{state.number}</h1>
        </div>
    );
};

export default Info;

useMemo

import React, { useMemo } from 'react';

const memoizedValue = useMemo(() => fn(a, b), [a, b]);
  • 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술을 메모이제이션(Momoization)이라고 한다.
  • useMemo()는 메모이제이션된 값을 반환하여 동일 계산의 반복 수행을 최소화하기 위한 Hook이다. useMome()는 특정 데이터가 변경 되었을 때에만 매개변수로 전달받은 함수를 실행한다. 이 최적화는 모든 렌더링 시의 고비용 계산을 방지하게 해준다.
  • useMemo()에 전달된 함수는 렌더링 중에 실행되며, 사이드 이펙트(DOM조작, 데이터 불러오기 등)같이 렌더링 중에 할 수 없는 작업들은 함수 내에 작성하지 않는다. (사이드 이펙트를 다루는 작업은 useEffect에서 해야한다.)
  • useMemo()는 렌더링하는 과정에서 특정 값이 변경되었을 때만 연산을 실행하고, 값이 변경되지 않았다면 이전에 연산했던 결과를 다시 사용한다.
  • 의미상의 보장이 아닌 성능 최적화로 useMemo()를 사용할 수 있다. 리액트는 useMemo()의 메모이제이션 방식을 변경할 수도 있기 때문에, useMemo()를 사용하지 않고도 동작할 수 있도록 코드를 작성하고, 작성된 코드를 추가하여 성능을 최적화 할 것을 권장하고 있다.
  • useMemo()는 첫 번째 매개변수로 값이 변경 될 때마다 실행될 함수를, 두 번째 매개변수로는 배열을 갖는다. 이 배열에는 어떤 state가 변경되었을 때 첫 번째 매개변수를 실행할 지를 명시해야한다.(리액트에서는 이를 의존성 값이라고 한다.) 두 번째 매개변수가 없는 경우 매 렌더링 때마다 새 값을 계산한다.
  • 실행함수는 두 번째 매개변수에서 할당한 값을 매개변수로 갖는다. 위 코드에서는 a, b가 변결될 때만 fn() 함수가 실행된다.
  • import React, { useState, useMemo} from 'react';
    
    const getAverage = numbers => {
        console.log('평균값 계산중');
    
        const length = numbers.length;
    
        if(length == 0) return;
    
        const sum = numbers.reduce((a, b) => a + b);
        return sum/length;
    }
    
    const Info = () => {
        const [ list, setList ] = useState([]);
        const [ number, setNumber ] = useState('');
    
        const changeValue = (e) => {
            setNumber(e.target.value);
        }
    
        const addList = () => {
            const nextList = [...list, parseInt(number)];
    
            setList(nextList);
            setNumber('');
        }
    
        const avg = useMemo(() => getAverage(list), [list])
    
        return (
            <div>
                <input name="number" value={number} onChange={changeValue}/>
                <button onClick={addList}>추가</button>
                <ul>
                    {
                        list.map((number, index) => 
                            <li key={index}>{number}</li>
                        )
                    }
                </ul>
                <div>평균값 : {avg}</div>
            </div>
        );
    };
    
    export default Info;

useCallback

import React, { useCallback } from 'react';

const memoizedCallback = useCallback(() => fn(a, b), [a, b]);
  • 컴포넌트에 만들어진 함수는 컴포넌트가 리렌더링 될 때마다 새로 만들어진 함수를 사용한다. 대부분 이러한 방식은 크게 문제가 없지만, 컴포넌트의 렌더링이 자주 발생하거나 렌더링해야할 컴포넌트의 개수가 많아지면 최적화가 필요하다.
  • 현재 하위 컴포넌트에 전달하는 콜백 함수를 inline 함수로 사용하고 있다거나, 컴포넌트 내에서 함수를 생성하고 있다면 (프로그래밍 구동에는 문제가 없겠지만) 새로운 함수 참조 값을 계속해서 만들고 있는 것, 다시 말해 똑같은 모양의 함수를 계속해서 만들어 메모리에 계속해서 할당하고 있다는 것을 뜻한다.
  • 특히 함수를 props로 전달받는 경우, 상위 컴포넌트의 state가 변경되면 상위 컴포넌트가 리렌더링 되며 하위 컴포넌트에 넘겨주는 props가 새롭게 생성되고 call by reference에 의해 새로운 props가 이전 props와 다르다고 판단해 리렌더링을 일으킨다. 의존성에 포함된 값이 변경되지 않는다면 이전에 생성한 함수 참조 값을 반환해주는 것이 useCallback()이다. 즉 컴포넌트의 props로 넘겨주는 함수는 useCallback 사용해야 최적화가 된다.
  • useMemo()는 메모이제이션된 값을 반환하여 동일 계산 반복수행을 최소화해 최적화를 한다면, useCallback()는 메모이제이션된 콜백을 반환하여 새로운 함수가 생성되는 것을 줄여 최적화를 한다.
  • 위의 코드에서 memoizedCallback에 할당되는 값은 a, b 값이 수정될 때에만 inline callback 이 새로 생성되는 함수다. 즉 함수를 생성하는 함수인 것이다. 이때 의존성 값에 빈 배열을 주면 최초에 생성된 함수를 지속적으로 기억한다.
  • 다시 말해 useCallback()은 최초에 혹은 특정 조건에서 생성한 함수의 참조를 기억하여 반환해주는 hook이다. 새로 생성되지 않는다함은 메모리에 새로 할당되지 않고 동일 참조 값을 사용하게 된다는 것을 의미하고, 이는 최적화된 하위 컴포넌트에서 불필요한 렌더링을 줄일 수 있다는 것을 뜻한다.

인라인 함수 방식의 문제점

const Parent = () => {
  return (
    <>
      <Child onClick={() => console.log('callback')}/>
      <Child onClick={() => console.log('callback')}/>
      <Child onClick={() => console.log('callback')}/>
      <Child onClick={() => console.log('callback')}/>
      <Child onClick={() => console.log('callback')}/>
	  ... // 똑같은 함수가 리랜더링 될 때마다 계속해서 재 생성된다.
    </>
  );
};
// 렌더되는 Child 컴포넌트의 수 만큼 인라인 함수가 생성된다. 
const Child = ({onClick}) => {
  return <button onClick={onClick}>Click Me!</button>
};
  • 위와 같이 child 컴포넌트의 클릭 이벤트로 콜백 함수를 넘겨주는 상황에서 인라인 함수를 사용하게 된다면 Child컴포넌트가 렌더되는 모든 시점에 새로 메모리에 할당되는 똑같은 모양의 인라인 함수들이 계속해서 재생성된다.

내부 함수 방식의 문제점

const Parent = () => {
  const _onClick = () => {
    console.log('callback');
  };
  
  return (
    <>
      <Child onClick={_onClick}/>
      <Child onClick={_onClick}/>
      <Child onClick={_onClick}/>
	  ... // 로컬 함수인 _onClick()을 하위 컴포넌트가 참조하고 있다.
    </>
  );
};

// Child이 여러 번 생성되더라도 onClick props으로 전달되는 _onClick 함수는 한번만 생성된다. 
const Child = ({onClick}) => {
  return <button onClick={onClick}>Click Me!</button>
};
  • 위 코드처럼 내부 함수를 생성하여 하위 컴포넌트에 props로 내려주는 이 방식은 인라인 함수 방식보다는 나은 방식이다. 하지만 내부 함수를 감싸고 있는 함수가 리렌더링 될 때, 내부 함수 역시 새로만들어 진다. 즉 위 코드의 Parent 컴포넌트가 리랜더링 되면 내부 함수 _onClick() 함수가 새로 생성되는 것이다.

useCallback 적용

const Root = () => {
  const [isClicked, setIsClicked] = useState(false);
  const _onClick = useCallback(() => {
    setIsClicked(true);
  }, []); 
// dependency가 없으므로 Root component가 렌더링 되는 최초에 한번만 생성되며 
// 이후에는 동일한 참조 값을 사용한다.
  
  return (
    <>
      <Child onClick={_onClick}/>
      <Child onClick={_onClick}/>
      <Child onClick={_onClick}/>
	  ...
    </>
  );
};

// Root와 Child가 여러번 렌더링 되더라도 onClick props으로 전달되는 _onClick 함수는 
// 한번만 생성되므로 계속해서 동일 참조 값을 가진다. 
const Child = ({onClick}) => {
  return <button onClick={onClick}>Click Me!</button>
};
  • import React, { useState, useMemo, useCallback} from 'react';
    
    const getAverage = numbers => {
        console.log('평균값 계산중');
    
        const length = numbers.length;
    
        if(length == 0) return;
    
        const sum = numbers.reduce((a, b) => a + b);
        return sum/length;
    }
    
    const Info = () => {
        const [ list, setList ] = useState([]);
        const [ number, setNumber ] = useState('');
    
        const changeValue = useCallback((e)=>{
            setNumber(e.target.value);
        }, [])
    	// 기존 값을 조회하지 않고 설정만하기 때문에
    	// 해당 컴포넌트가 처음 렌더링 될 때 생성된 함수를 사용
    
        const addList = useCallback(()=>{
            const nextList = [...list, parseInt(number)];
            setList(nextList);
            setNumber('');
        }, [number, list])
    	// number, list를 조회하여 새로운 배열을 생성하기 때문에
    	// 의존성 값에 number와 list를 추가한다.
    
        const avg = useMemo(() => getAverage(list), [list])
    
        return (
            <div>
                <input name="number" value={number} onChange={changeValue}/>
                <button onClick={addList}>추가</button>
                <ul>
                    {
                        list.map((number, index) => 
                            <li key={index}>{number}</li>
                        )
                    }
                </ul>
                <div>평균값 : {avg}</div>
            </div>
        );
    };
    
    export default Info;

useRef

import React, { useRef } from 'react';

const refContainer = useRef(initialValue);
  • useRef() Hook은 함수형 컴포넌트에서 ref를 쉽게 사용할 수 있게 해준다.
  • React.createRef()로 ref 객체를 생성하고 해당 객체의 current 프로퍼티를 이용해 해당 DOM을 조작하는 방식과 비슷하다.
    this.myRef = React.createRef(); // ref 생성
    
    return <div ref={this.myRef} /> // ref 어트리뷰트를 통해 React 엘리먼트에 부착
    
    this.myRef.current // current 프로퍼티로 DOM 조작
  • function TextInputWithFocusButton() {
      const inputEl = useRef(null); // ref 객체 생성
      const onButtonClick = () => {
        // `current` 프로퍼티는 input을 가리킨다.
        inputEl.current.focus();
      };
      return (
        <>
          <input ref={inputEl} type="text" /> 
    			{ // ref 어트리뷰트를 통해 React 엘리먼트에 부착 }
          <button onClick={onButtonClick}>Focus the input</button>
        </>
      );
    }

useRef() 로 컴포넌트 안의 변수 만들기

  • useRef() Hook 은 DOM 을 선택하는 용도 외에도, 컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리하는 용도로 사용할 수 있다. useRef 로 관리하는 변수는 값이 바뀐다고 해서 컴포넌트가 리렌더링 되지않는다.
  • 리액트 컴포넌트에서의 state는 상태를 바꾸는 함수(setState 등)를 호출하고 나서 그 다음 렌더링 이후로 업데이트 된 상태를 조회 할 수 있는 반면, useRef() 로 관리하고 있는 변수는 설정 후 바로 조회 할 수 있다.
  • 즉 state처럼 컴포넌트를 다루는데 필요한 데이터지만 컴포넌트 렌더링 없이 데이터를 다루고 싶을 때 사용할 수 있다.
  • useRef()로 생성된 변수를 사용하여 다음과 같은 값을 관리 할 수 있다.
    • setTimeoutsetInterval 을 통해서 만들어진 id
    • 외부 라이브러리를 사용하여 생성된 인스턴스
    • scroll 위치
  • 예) useRef()로 리스트 id 생성
    import React, { useState, useRef } from 'react';
    
    const List = () => {
        const [input, setInput] = useState('');
        const [items, setItems] = useState([
            {id: 1, text: '눈사람'},
            {id: 2, text: '얼음'},
            {id: 3, text: '눈'},
            {id: 4, text: '바람'},
        ]);
    
        const nextId = useRef(5); // 초기값을 부여해 nextId 생성
    
        const handleInput = e => {
            setInput(e.target.value);
        }
    
        const itemsList = items.map(name => {
            return <li key={name.id}>{name.text}</li>
        });
    
        const handleClick = () => {
            if(input.trim() === '') return;
            const nextItems = items.concat({
                id: nextId.current, // id 추가할 때 currnt 프로퍼티로 접근
                text: input
            });
    
            nextId.current += 1; //id 수정도 current 프로퍼티 사용
    				// 값을 변경해도 리랜더링 되지 않음
            
            setItems(nextItems);
            setInput('');
        }
    
        return (
            <div>
                <input 
                    value={input} 
                    onChange={handleInput} 
                />
                <button onClick={handleClick}>추가</button>
                <ul>
                    {itemsList}
                </ul>
            </div>
        );
    };
    
    export default List;

커스텀 Hook

  • 컴포넌트를 만들다보면 input을 관리하는 코드처럼 반복되는 로직이 자주 발생한다. 이러한 상황에서 커스텀 Hook을 만들면 반복되는 로직을 줄이고 같은 로직을 쉽게 재사용할 수 있다.
  • 커스텀 Hook를 만들때 보통 use라는 키워드로 시작하는 파일을 만들고 그 안에 함수를 작성한다. 커스텀 Hook은 파일 내부에서 useState, useEffect, useReducer, useCallback 등 Hooks 를 사용하여 원하는 기능을 구현하고, 컴포넌트에서 사용하고 싶은 값들을 반환해주면 된다.
  • 예)
    // src/hooks/useInputs.js
    import { useReducer } from 'react';
    
    function reducer(state, action) {
        return {
            ...state,
            [action.name]: action.value
        }
    }
    
    export default function useInputs(initialForm) {
        const [state, dispatch] = useReducer(reducer, initialForm);
        const onChange = (e) => {
            dispatch(e.target);
        }
    
        return [state, onChange];
    };
    
    // src/Info.js
    import React from 'react';
    import useInputs from './hooks/useInputs';
    
    const Info = () => {
        const [ state, onChange ] = useInputs({
            name: '',
            number: ''
        })
    
        return (
            <div>
                <input name="name" value={state.name} onChange={onChange}/>
                <input name="number" value={state.number} onChange={onChange}/>
    
                <h1>name : {state.name}</h1>
                <h1>number : {state.number}</h1>
            </div>
        );
    };
    
    export default Info;
profile
애기 프론트 엔드 개발자

2개의 댓글

comment-user-thumbnail
2022년 5월 10일

React 공부 중이었는데 정리 잘 하셨네요! 잘 보고 갑니다 :)

1개의 답글