Hooks

nasagong·2023년 2월 14일
0

React

목록 보기
9/15
post-thumbnail

📚 들어가며

드디어 Hooks다. 클래스형 컴포넌트로 실습을 진행할 때마다 곧 Hooks를 배우게 되면 함수형으로도 다 구현 가능하다는 교재의 달램(?)을 받으며 진도를 뺐는데 생각보다 금방 마주하니 뻘쭘


Hooks는 함수형 컴포넌트에서도 클래스형 컴포넌트에서만 가능하던 state관리를 할 수 있도록 도와주는 기능이다. 리액트의 내장 hooks들을 먼저 사용해보고 나만의 hooks도 만들어보자.

1. useState

얘는 인간적으로 생략하자..
어차피 여기서 다 했다

2. useEffect

리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 하는 Hooks다. 코드로 확인해보자.

import {useState,useEffect} from 'react';

const Info = () =>{
    const [form,setForm]=useState({
        name:'',
        nickname:''
    });
    useEffect(()=>{
        alert('렌더링이 완료되었습니다!');
        alert(form.name);
        alert(form.nickname);
    })
    const onChange = e =>{
        const nextForm={
            ...form,
            [e.target.name] : e.target.value
        }
        setForm(nextForm);
    };
    return(
        <div>
            <input
            name="name"
            value={form.name}
            onChange={onChange}
            placeholder="name"
            />
            <input
            name="nickname"
            value={form.nickname}
            onChange={onChange}
            placeholder="username"
            />
            <h1>{form.name}</h1>
            <h1>{form.nickname}</h1>
        </div>
    )
}
export default Info;

저번에 구현했던 이름/닉네임을 입력받고 입력받은 값을 실시간으로 보여주는 기능의 컴포넌트다. 이번엔 추가로 useEffect가 사용되었는데 처음 렌더링될 때, state값이 업데이트 될 때마다 미리 설정해둔 알림창을 띄운다.

뭔가 떠오르지 않는가? componentDidMount, componentDidUpdate를 합친듯한 기능을 하고있다.

처음 마운트될 때 알림창을 두 번 띄우는데, 이는 React.strictMode가 적용돼 있는 경우 그렇다. useEffect를 사용한 코드에 문제가 없는지 감지하기 위해서라고 한다.

서버 문제로 console.log대신 alert를 사용한 내 잘못이지만.. 자꾸 창이 나타나니 어째 영 정신 사납다. 마운트될 때만 useEffect가 호출되게 하면 안 될까? 당연히 된다!

방법은 단순하다. useEffect의 두번째 파라미터로 빈 배열을 넣어주면 끝이다. 한 번 수정해볼까?

useEffect(()=>{
        alert('렌더링이 완료되었습니다!');
    },[]);

이제 마운트 후에만 알림창이 뜨고 state가 업데이트 돼도 반응하지 않는다.

그렇다면.. name이 변화될 때는 반응하지 않고 username이 바뀔 때만 실행되게 할 수는 없을까? 이 기능 역시 당연히 지원한다. 이번엔 두번째 피라미터에 전달되는 배열 안에 검사하고 싶은 값을 넣어주면 된다. 작성해보자!

useEffect(()=>{
        alert('어이어이 닉네임을 바꾼거냐고?');
    },[form.nickname]);

nickname과 username을 섞어 쓰고 있다는 걸 지금 알아차렸는데 일단 넘어가자.. 렌더링해보면 실제로 nickname상태가 업데이트 될 때 useEffect가 호출된다. 물론 마운트 될 때도 여전히 호출된다.

처음 useEffect를 소개할 땐 두번째 파라미터가 아예 없는 코드를 보여줬는데, 이런 상황은 보통 잘 없고 빈 배열을 넣더라도 일단 배열이 들어가는 경우가 더 많다고 한다.

뒷정리하기

컴포넌트가 언마운트되거나 업데이트되기 직전에 특정 작업을 수행하게 만들고 싶다면 어떡해야 될까. cleanup, 즉 뒷정리 함수를 반환해주면 된다. useEffect를 조금 수정해보자.

useEffect(()=>{
        alert('effect');
        alert(form.name);
        return ()=>{
            alert('cleanup');
            alert(form.name);
 }

App컴포넌트도 좀 수정해보자.

import Info from './Info'
import {useState} from 'react';
const App = () =>{
  const [visible,setVisible] = useState(false);
  return (
    <div>
      <button
      onClick={
        ()=>{
          setVisible(!visible);
        }
      }>
        {visible? '숨기기' : '보이기'}
      </button>
      <hr/>
      {visible&&<Info/>}
    </div>
  );
};
export default App;


보이기 버튼을 누르면 Info컴포넌트를 보여주고, 숨기기 버튼을 누르면 숨긴다. 이 때 name값을 건드려보면 cleanup함수가 먼저 호출돼 업데이트되기 전 상태를 보여준다. 숨기기 버튼으로 Info를 언마운트할 때도 마찬가지로 사라지기 전 마지막 name값을 보여준다. 이처럼 useEffect에서 claenup함수를 반환하면 업데이트/언마운트 되기 전 상태를 다룰 수 있게 된다.

오직 언마운트 될 때만 클린업 함수를 호출하고 싶다면 검사값을 비워주면 된다. 두번째 파라미터인 배열을 비워주면 된다는 거다. 이건 생략하자.

3. Reducer

리듀서는 현재 상태, 업데이트를 위해 필요한 정보를 담은 action 값을 전달받아 새로운 상태를 반환하는 함수다. 리듀서를 사용할 땐 반드시 불변성을 지켜야한다.

function reducer(state,action){
    reuturn {...};
}

기본적으로 이런 형태인데, 역시 코드를 보는 게 빠를 것 같다. 리듀서를 사용해 카운터를 구현해봤다.

import { 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>
                현재 카운터 값은 <b>{state.value}</b>입니다.
            </p>![](https://velog.velcdn.com/images/nasagong/post/b1450cd3-dddf-4cf6-81b6-4f4065370eb0/image.jpeg)
            <button onClick={()=>dispatch({type:'INCREMENT'})}>+1</button>
            <button onClick={()=>dispatch({type:'DECREMENT'})}>-1</button>
        </div>
    )
}
export default Counter;

useReducer의 첫번째 파라미터에는 리듀서 함수를 넣고, 두번째에는 해당 리듀서의 기본 값을 넣어준다. 여기서는 {value:0}이다. 이 hook은 상태와 dispatch함수를 반환한다.

dispatch함수는 액션값을 피라미터로 받아 리듀서 함수를 호출하는 역할을 한다.

조금 정신없는데.. 일단 액션값이 뭔지 타입이 뭔지에 대한 설명도 딱히 없다. 이 부분은 리덕스에 대해 공부할 때 알게 될거라 일단은 그렇구나 모드로 넘어가자.

userReducer에 리듀서 함수와 state의 초기값을 설정해주면 state를 사용할 수 있게 되며, 반환받은 dispatch 함수를 통해 리듀서 함수에 액션값을 넘길 수 있다.

이 때 액션값에 따라 리듀서 함수는 state를 업데이트한다.
위 코드에선 action을 “type:..." 형태로 전달해 리듀서 함수에서 swtich문으로 판단 후 상태를 업데이트 하도록 만들고 있다.

리듀서로 인풋 상태 관리하기

Info 컴포넌트를 재탕해보자. 이번엔 리듀서를 사용해 인풋 상태를 관리해 볼 것이다. useReducer에서 액션 값은 어떤 값이리도 사용 가능하다. 따라서 이번엔 이벤트 객체의 target 자체를 액션으로 넘겨 리듀서 함수에서 상태를 관리하도록 만들어 볼 것이다. 물론 불변성을 유지하면서..!

import {useReducer} from 'react';

const Info = () =>{
    function reducer(state,action){
        return{
            ...state,
            [action.name]:action.value
        };
    }
    const [state,dispatch]=useReducer(reducer,{
        name:'',
        nickname:''
    });
    const {name,nickname} = state;
    const onChange = e =>{
        dispatch(e.target);
    };
    return(
        <div>
            <input
            name="name"
            value={name}
            onChange={onChange}
            placeholder="name"
            />
            <input
            name="nickname"
            value={nickname}
            onChange={onChange}
            placeholder="nickname"
            />
            <h1>이름:{name}</h1>
            <h1>닉네임:{nickname}</h1>
        </div>
    )
}
export default Info;

dispatch함수로 e.target을 리듀서 함수에 넘겼고, 리듀서 함수에선 action.name , action.value같은 형태로 쉽게 인풋값에 접근해 상태를 관리하고 있다. useState보다 좀 더 간단하게 느껴진다. 리듀서 사용 후에도 전과 기능은 같기에 렌더링 결과는 생략하겠다.

4. useMemo

설명에 앞서 우선 useState를 사용해 인풋으로 입력한 값들의 평균값을 구하는 페이지를 만들어보자.

import { useState } from "react";

const getAverage = numbers =>{
    alert('평균값 계산중..');
    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 = (e) => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber("");
  };
  return (
    <div>
      <input vlaue={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값</b>
        {getAverage(list)}
      </div>
    </div>
  );
};
export default Average;

작동이야 잘 하는데, 렌더링될 때마다 시도때도 없이 getAverage 함수가 호출돼서 매우 정신사납다(반환값에서 함수를 호출하고 있기 때문에 그렇다). 필요할 때만 나오게 할 수는 없을까? 이런 상황에는 useMemo를 사용하면 좋다.

useMemo를 적용한 부분만 확인해보자.

const Average = () =>{

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

reutrn(
...
<div>
     <b>평균값</b> {avg}
</div>
...
 )
}

useEffect처럼 두번째 파라미터를 검사하고, 그 값이 변경됐을 때만 콜백함수를 호출하도록 만들어 쓸데없이 함수가 여러번 호출되는 것을 예방했다.

5. useCallback

useCallback은 useMemo와 비슷하게 최적화를 위해 사용된다. 앞서 작성한 코드에서 onChange, onInsert함수는 컴포넌트가 리렌더링 될 때마다 사용되고 있다. onChange함수 같은 경우 처음 렌더링될 때 그 상태를 계속 유지해도 문제가 없으며, onInsert는 number나 list가 변하는 게 아니면 다시 설정될 필요가 없다.

이럴 때 useCallback에 첫번째 피라미터로 사용될 콜백함수, 두번째 피라미터에는 검사값이 들어간 배열을 추가하면 필요할 때만 함수가 재설정된다. 이번에도 수정된 부분만 확인해보자.

const onChange = useCallback(e => {
    setNumber(e.target.value);
  },[]);
  const onInsert = useCallback(e => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
  },[number,list]);

설명한대로 useCallback을 사용했다.

6. useRef

함수형 컴포넌트에서도 Ref를 사용할 수 있게 만들어주는 hook이다. Ref를 사용해 Average컴포넌트에서 등록 버튼을 누르면 입력창으로 포커스가 이동하도록 만들어보자.
(역시나 수정된 부분만 작성한다)

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');
  const inputEI = useRef(null);

  const onChange = useCallback(e => {
    setNumber(e.target.value);
  },[]);
  const onInsert = useCallback(() => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
    inputEI.current.focus();
  },[number,list]);

  const avg = useMemo(()=>getAverage(list),[list]);
  return (
    <div>
      <input value={number} onChange={onChange} ref={inputEI} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값</b>
        {avg}
      </div>
    </div>
  );
};

ref를 선언해주고, input에 해당 ref를 할당해준 후, onInsert에 focus함수를 추가해줬다.간단하다! 이제 함수형 컴포넌트에서도 ref를 사용할 수 있게 되었다.

로컬 변수 사용하기

로컬 변수란 렌더링과 무관한 변수를 말한다. class형 컴포넌트로 예시를 들어보자면 아래와 같다.

class MyComponent extends Component{
    id = 1
    setId = n =>{
        this.id = n;
    }
    printId = () =>{
        console.log(this.id);
    }
    render(){
        return(
            <div>
                MyComponent
            </div>
        );
    }
}

여기서 로컬변수는 id다. class인스턴스에선 this 바인딩을 통해 쉽게 로컬변수에 접근해 사용할 수 있다. 함수형 컴포넌트에선 ref를 통해 선언함과 동시에 사용할 수 있다.

const RefSample = () =>{
    const id = useRef(1);
    const setId = n =>{
        id.current = n;
    }
    const printId = () =>{
        console.log(id.current);
    }
    return(
        <div>
            refSample
        </div>
    );
}

current?

딱히 설명도 없이 id.current가 나타났다. 너무 자연스러워서 원래 알고있었다고 생각할 뻔했다. 뭔데 id에서 current를 접근하는 걸까?

useRef는 객체를 반환한다. 이 객체에는 ref값을 보여주는 current 프로퍼티가 있으며, ref값을 업데이트할 때 사용된다. 즉 useRef(1)가 호출된 순간 id.cuurent는 1이 된다.

위 방식은 렌더링과 관련되지 않은 값을 관리할 때만 사용하도록 하자.

7. 나만의 Hooks 만들기

여러 컴포넌트에서 비슷한 기능을 공요할 경우 직접 hook을 작성해 로직을 재사용할 수 있다. Info 컴포넌트를 직접 만든 hook을 사용해 다시 작성해보자

//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];
}

useReducer와 리듀서 함수를 hook에 포함시켜버렸다. 이를 Info 컴포넌트에 적용하면 아래와 같다.

onst Info = () =>{
    const [state,onChange] = useInputs({
        name:'',
        nickname:''
    });
    const {name,nickname} = state;
    /*function reducer(state,action){
        return{
            ...state,
            [action.name]:action.value
        };
    }
    const [state,dispatch]=useReducer(reducer,{
        name:'',
        nickname:''
    });
    const {name,nickname} = state;
    const onChange = e =>{
        dispatch(e.target);
    };
    */
    return(
        <div>
            <input
            name="name"
            value={name}
            onChange={onChange}
            placeholder="name"
            />
            <input
            name="nickname"
            value={nickname}
            onChange={onChange}
            placeholder="nickname"
            />
            <h1>이름:{name}</h1>
            <h1>닉네임:{nickname}</h1>
        </div>
    )
}

주석으로 가려둔 부분은 useInputs 사용으로 인해 정리된 부분이다. 코드가 훨씬 깔끔해졌다. 지금은 예시를 위해 단일 컴포넌트에 사용하는 상황을 차용했지만 반복되는 작업이 있다면 hook을 직접 만들어 사용해보는 것도 좋아보인다. 깃허브 등을 통해 타인이 만든 hook을 가져올 수도 있다.


📚 마치며

상당한 분량... 실습 해봤다고 대강 넘어가지 말고 반드시 복습하도록 하자... 이제 hooks도 배웠으니 가급적 함수형 컴포넌트로 프로젝트를 작성하는 습관을 들이자.

profile
잘쫌해

0개의 댓글