본문 바로가기
Tech/React

[React] Hooks

by 소라소라잉 2022. 8. 18.

1. Hook의 정의 및 종류

리액트 v16.8에 도입된 기능으로, 클래스형 컴포넌트에서만 가능했던 작업들(상태관리, 렌더링 직후 작업 설정 등)을 함수형 컴포넌트에서도 사용할 수 있게 되었다.

  • useState
  • useEffect
  • useReducer
  • useMemo
  • useCallback
  • useRef

2. useState

가장 기본적인 Hook으로 컴포넌트가 가변적인 상태를 지닐 수 있게 해준다. 함수형 컴포넌트에서 상태관리가 필요하다면 useState를 사용한다.

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

useState()가 호출되면 배열을 반환하는데, 배열의 첫 번째 원소는 상태 값(value), 두 번째 원소는 상태설정을 위한 함수가 반환된다. 함수의 파라미터에는 상태의 기본값을 넣어준다.

따라서 위 예제의 const value는 0으로 초기화되며, const setValue에는 함수가 대입된다.

하나의 useState()함수는 하나의 상태 값만 관리할 수 있으므로, 컴포넌트에서 관리해야 할 상태가 여러개라면 여러개의 useState()를 사용한다.

const [name, setName] = useState("");
const [nickName, setNickName] = useState("");
const onChangeName = (event) => { setName(event.target.value); }
const onChangeNickName = (event) => { setNickName(event.target.value); }

return (
    <div>
        <p>이름 : {name}</p> <input value={name} onChange={onChangeName}></input>
        <p>닉네임 : {nickName}</p> <input value={nickName} onChange={onChangeNickName}></input>
    </div>
);

3. useEffect

리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 수행하도록 설정하는 함수이다. 클래스형 컴포넌트의 componentDidMount와 componentDidUpdate를 합친 형태로 볼 수 있다.

메서드의 파라미터로 수행할 작업 함수를 넣어주면 된다.

useEffect는 기본적으로 모든 렌더링이 발생할때마다 호출되는데, 아래와 같이 두번째 파라미터를 조율하여 실행 조건을 별도로 설정해 줄 수도 있다.

  • 렌더링(최초/업데이트)될 때마다 실행하도록 설정하는 방법
useEffect(() => {
    console.log("렌더링 될때마다 실행됩니다.")
})
  • 최초 렌더링 될 때만(마운트 될 때만) 실행하도록 설정하는 방법
useEffect(() => {
    console.log("마운트될 때만 실행됩니다.")
},[])
  • 특정 값이 변경될 때만(업데이트 될 때만) 실행하도록 설정하는 방법
useEffect(()=>{
    console.log('name값이 변경될 때만 실행됩니다. name :: '+name);
},[name])

/*
// 업데이트 대상을 여러개 설정할 수도 있다. 
useEffect(()=>{
   console.log("name과 nickName이 변경될 때만 실행됩니다. name :: "+name+', nickName :: '+nickName);
},[name,nickName])
*/
  • 컴포넌트가 언마운트되기 전이나, 업데이트 되기 직전에 실행하도록 설정하는 방법
useEffect(()=>{
    console.log('name값이 변경될 때만 실행됩니다. name :: '+name);
    return () => {
        console.log('cleanup 함수입니다. 컴포넌트가 언마운트되기 직전이나' + 
                    '업데이트되기 직전에 수행됩니다.'+name);
    }
},[name])

* 특정값이 변경될 때만 실행되는 작업과, cleanup함수로 언마운트 되기 전 & 업데이트 되기 전의 작업을 추가한 후 console창

  • 특정값이 변경될 때만 실행되는 작업과, cleanup함수로 언마운트 되기 전 & 업데이트 되기 전의 작업을 추가한 후 console창

useEffect()는 기본적으로 렌더링되고 난 직후마다 실행되며, 두 번째 파라미터 배열에 무엇을 넣는지에 따라 실행 조건이 달라지는데, 함수내에 return 값으로 함수를 반환하면, 그 함수는 컴포넌트가 언마운트 되기 전이나 업데이트 되기 전에 수행된다. 이때 return으로 반환하는 함수를 뒷정리(cleanup)함수라 한다.

4. useReducer

useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트하고 싶을 때 사용한다.

reducer는 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션(action)값을 전달 받아 새로운 상태를 반환하는 함수이다. reducer 함수에서 새로운 상태를 만들 때는 반드시 불변성을 지켜주어야 한다.

function reducer(state, action) {
    return {...} // 불변성을 지키면서 업데이트한 새로운 상태를 반환한다. 
}

액션 값은 type필드를 포함한 객체 형태이다. useReducer에서 사용하는 액션 객체는 반드시 type필드를 지니고 있을 필요가 없지만, 추후 다룰 리덕스에서 사용하는 액션 객체에는 어떤 액션인지 알려주는 type필드가 꼭 있어야 한다.

useReducer()의 첫 번째 파라미터에는 리듀서 함수를 넣고, 두 번째 파라미터에는 해당 리듀서의 기본값을 넣어준다.

‘리듀서 함수'라 함은, ‘업데이트된 상태(state)를 return 해주는 함수’라고 생각하면 쉽다.

어떤 업데이트 로직이 짜여있고, 해당 로직에 의해 업데이트된 상태값을 return 해주는 리듀서 함수를 useReducer()의 첫번째 파라미터로 넣고, state의 초기값은 두번째 파라미터로 넣어주는 것이다.

그리고 리듀서 함수(reducer(state,action))의 첫번째 파라미터는 현재 state, 두번째 파라미터는 값을 변경하는데 필요한 객체(action)를 받도록 되어있는데, dispatch라는 함수를 이용하면 현재 state가 자동으로 리듀서함수의 첫번째 파라미터로 대입되고, dispatch(action)에 넣어주는 단일 파라미터인 action 객체가 리듀서함수의 두번째 파라미터로 매핑된다.

function reducer (state, action) {
    return {...}
}

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

useReducer Hook을 사용하면 현재 가리키고 있는 상태인 state값과 액션을 발생시키는 함수인 dispatch 함수를 리턴받는다.

업데이트된 상태(state)를 return해주는 리듀서 함수의 파라미터로, state를 업데이트 하는데 필요한 어떤 객체를 넣어주는데, 그 객체를 action이라고 부르는 듯 하다. 아무튼 dispatch(action)함수를 호출하여 리듀서 함수가 실행된다.

useReducer의 가장 큰 장점은 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다는 점이다.

/*
    reducer 함수로 컴포넌트의 업데이트 로직을 분리했다.
  dispatch 함수를 통해 reducer함수를 호출한다. 이때 dispatch의 파라미터로 넣어주는 액션값이
  reducer 함수의 두번째 파라미터(action)로 매핑된다. 
  첫번째 파라미터에는 현재 상태가 넘어온다. 
*/
const reducer = (state, action) => {
    return {
        ...state,
        [action.name] : action.value
    };
}

const Hook_useReducer2 = () => {
    const [state, dispatch] = useReducer(reducer, {
        name: '',
        nickName: ''
    });
    const {name, nickName} = state;
    const onChange = event => {
        dispatch(event.target);
    }

    return (
        <div>
            <div>
                <input name="name" value={name} onChange={onChange}/>
                <input name="nickName" value={nickName} onChange={onChange}/>
            </div>
            <div>
                <b>이름 : </b>{name}
                <b>닉네임 : </b>{nickName}
            </div>
        </div>);
}

useReducer에서의 액션은 그 어떤 값도 사용할 수 있다. 위 코드에서는 액션 값으로 event객체가 지닌 target값 자체를 사용하여, input box가 더 많아져도 코드를 짧고 깔끔하게 유지 할 수 있다.

useReducer를 이용하여 custom hook 만들기

reducer를 이용하여 custom hook을 만들어 조금 더 간결하고 편리하게 컴포넌트를 이용 할 수 있다.

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 = event => {
        dispatch(event.target);
    };
    return [state, onChange];
}

export default useInputs;

위 코드에서 아예 reducer 부분을 분리하여 하나의 컴포넌트로 만든 useInput이다. 컴포넌트의 return 값으로 state와 onChange함수를 반환하여 필요한 엘리먼트에 적용해주면 아무리 input이 늘어나도 코드가 지저분해지지 않고 편리하게 사용할 수 있다.

import useInputs from "./useInputs";

const UseCustomHook = () => {

    const [state, onChange] = useInputs({
        'name' : '',
        'nickName' : '',
        'age' : '',
        'gender' : ''
    })

    const {name, nickName, age, gender} = state;

    return (
        <div>
            <b>name : </b>{name}
            <br/>
            <b>nickName : </b>{nickName}
            <br/>
            <b>age : </b>{age}
            <br/>
            <b>gender : </b>{gender}
            <br/>
            <b>name - </b><input name='name' value={name} onChange={onChange}></input><br/>
            <b>nickName - </b><input name='nickName' value={nickName} onChange={onChange}></input><br/>
            <b>age - </b><input name='age' value={age} onChange={onChange}></input><br/>
            <b>gender - </b><input name='gender' value={gender} onChange={onChange}></input><br/>
        </div>
    );

}

export default UseCustomHook

5. useMemo

react에서는 어떤 state가 바뀔 때 마다 리렌더링을 실행하는데, 리렌더링을 하는 과정에서 필연적으로 연관된 메서드를 실행하는 경우가 있다. 컴포넌트 내부에서 발생하는 이런 비효율적인 연산을 최적화 하고자 사용하는 것이 useMemo이다.

렌더링하는 과정에서 설정한 특정 값이 바뀌었을 때만 연산을 실행하고, 원하는 값이 바뀌지 않았다면 이전에 연산했던 결과를 다시 사용하는 방식으로 동작한다.(함수자체를 저장하는게 아니라 결과값만 저장)

아래의 경우, input값이 change될 때마다 state가 변경되어 리렌더링이 실행되고, 리렌더링을 하는 과정에서 컴포넌트에 포함된 getAverage()함수를 필연적으로 호출하게 된다.

* input으로 숫자를 입력받아 평균값을 계산하는 컴포넌트

  • input으로 숫자를 입력받아 평균값을 계산하는 컴포넌트
const Hook_useMemo = () => {

    const getAverage = (list) => {
        if(list.length == 0) return 0;
        console.log("평균값 계산중...");
        const sum = list.reduce((a,b)=>a+b);
        return sum/list.length;
    }

    const [inputValue, setInputValue] = useState('');
    const [numberList, setNumberList] = useState([]);

    const onClick = () => {
        // parseInt 하지 않으면 문자열로 처리됨.
        const newList = numberList.concat(parseInt(inputValue))
        setNumberList(newList);
        setInputValue('')
    }

    return (
        <div>
            <input value={inputValue} onChange={
                                (e)=>{setInputValue(e.target.value)}}/>
            <button onClick={onClick}>등록</button>
            <ul>
                {numberList.map((value, index)=>(
                    <li key={index}>{value}</li>
                ))}
            </ul>

            <b>평균값 : </b> {getAverage(numberList)}
        </div>
    );
}

리렌더링하는 과정에서 불필요하게 평균값을 여러번 연산하게 되는데, 이때 useMemo를 사용하여 원하는 값(숫자의 리스트)이 변경될 때에만 특정 메서드를 실행할 수 있도록 할 수 있다.

착각하면 안되는게 리렌더링을 하지 않는 것이 아닌, 특정 연산(메서드의 실행)만을 제한할 수 있는 것이다.

const avg = useMemo(()=> getAverage(numberList), [numberList])

return (
        ...
    <b>평균값 : </b> {avg}
        ...
);

이렇게 수정하면 numberList가 변경될 때에만 getAverage함수가 호출된다.

6. useCallback

선언된 함수는 내부적으로 컴포넌트가 렌더링 될 때마다 새로 만들어져 쓰이게 된다. 렌더링이 자주 발생하거나 렌더링해야 할 컴포넌트의 개수가 많아지면 이런 부분을 최적화 해주는 것이 좋은데, 이때 쓰이는 것이 useCallback이다.

useCallback의 첫번째 파라미터에는 생성하고 싶은 함수를 넣고, 두번째 파라미터에는 배열을 넣는다. 두번째 파라미터의 배열로 함수 재생성 여부가 결정된다.

예를 들어 위 예제의 onChange의 경우에는 아래와 같이 useCallback의 두번째 파라미터로 빈 배열을 넣으면 컴포넌트가 처음 렌더링 될 때만 함수를 생성하고 계속해서 재사용하게 만들수 있다.

const onChange = useCallback((e)=>{
        setInputValue(e.target.value)
},[]) 

두번째 파라미터의 배열에 특정 state를 넣으면, 해당 state가 바뀌었을 때만 함수를 생성한다.

함수 내부에서 상태 값에 의존해야 할 때는 그 값을 반드시 두번째 파라미터안에 포함시켜 주어야 한다. 예를 들어 위 예제의 onChange함수는 기존의 값을 조회하지 않고 바로 설정만 하면 되기 때문에 빈 배열을 넣어도 무관하지만, onClick함수는 numberList와 inputValue에 의존적인 함수이기 때문에(값을 ‘조회'하기 때문에), 해당 state를 아래 코드처럼 꼭 포함시켜주어야 한다.

const onClick = useCallback(()=>{
    const newList = numberList.concat(parseInt(inputValue))
    setNumberList(newList);
    setInputValue('')
},[numberList, inputValue]) // numberList 혹은 inputValue가 바뀌었을 때만 함수 생성

7.useRef

HTML에서 id를 사용하여 DOM에 이름을 부여할 수 있는 것 처럼, 리액트 프로젝트 내부에서 DOM에 이름을 달아주기 위해선 ref를 사용한다.

리액트는 state를 사용하여 필요한 대부분의 기능을 구현할 수 있지만, 가끔 state만으로는 해결 할 수 없는 기능들이 있다. ‘DOM을 꼭 직접적으로 건드려야 할 때’인데, 대표적으로 아래 세가지의 경우가 그렇다.

  • 특정 input에 포커스 주기
  • 스크롤 박스 조작하기
  • Canvas 요소에 그림 그리기

위 작업은 state만 이용해서는 구현 할 수 없는 기능들로 클래스형 컴포넌트에서는 DOM에 ref값을 부여하고 React.createRef()로 DOM을 조작할 수 있었다면, 함수형 컴포넌트에서는 useRef를 사용한다.

useRef를 이용하여 위 예제에서 [등록]버튼을 클릭할 때 input box로 포커스를 넘긴다고 한다면, 아래처럼 useRef로 대상 요소에 삽입할 ref변수를 선언하고 input box에 ref를 달아 연결해주면 된다.

**const inputEl = useRef(null);**
...
const onClick = useCallback(() => {
        const newList = numberList.concat(parseInt(inputValue))
        setNumberList(newList);
        setInputValue('');
        **inputEl.current.focus();**
}, [numberList, inputValue])
... 
return (
    ...
    <input value={inputValue} onChange={onChange} **ref={inputEl}**/>
    ...
);

useRef를 사용하여 ref를 설정하면, useRef를 통해 만든 객체 안의 current값이 실제 엘리먼트(위의 예제에서는 input)를 가리킨다.

ref는 DOM의 엘리먼트에도 부여할 수 있지만, 로컬변수에도 부여할 수 있다. 하지만 로컬 변수는 리액트가 렌더링하기 위해 모니터링하는 state와 달리 그 값이 변경되어도 리렌더링 되지 않기 때문에, 렌더링과 관련되지 않은 값을 관리할 때만 사용해야 한다.

const id = useRef(1);
const setId = (n) => { id.current = n; }

'Tech > React' 카테고리의 다른 글

[React] 컴포넌트의 반복, 라이프사이클 메서드  (0) 2022.08.18

댓글