공부블로그

리액트 8강. Hooks 본문

리액트/리액트를 다루는 기술

리액트 8강. Hooks

떠어영 2021. 1. 14. 15:19

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

리액트 내장 Hooks를 사용하는 방법을 배우고 커스텀 Hooks를 만들어보자.

8. 1 ) useState

useState는 함수형 컴포넌트에서도 가변적인 상태를 지닐 수 있도록 해준다. ( 3장에서 배움 )

 

Counter. js

import React, { useState } from "react";

const Counter = () => {
  const [value, setValue] = useState(0);
  
  return (
    <div>
      <p>
        현재 카운터 값은 <b>{value}</b>입니다.
      </p>
      <button onClick={() => setValue(value + 1)}>+1</button> // value값 업데이트
      <button onClick={() => setValue(value - 1)}>-1</button>
    </div>
  );
};
export default Counter;

 

App. js

import React from "react";
import Counter from "./Counter";

const App = () => {
  return <Counter />;
};

export default App;

 

8. 1. 1 ) useState 여러번 사용하기 

 

Info. js

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>
        <div>
          <b>이름:</b> {name}
        </div>
        <div>
          <b>닉네임:</b> {nickname}
        </div>
      </div>
    </div>
  );
};
export default Info;

 

 

8. 2 ) useEffect

useEffect는 모든 렌더링이 완료된 후에 수행됩니다만, 어떤 값이 변경되었을 때만 실행되게 할 수도 있습니다.

 

 

8. 2. 1 ) 모든 렌더링이 완료된 후에 실행하고 싶을 때

 

useEffect에서 설정한 함수를 컴포넌트가 맨 처음 렌더링될 때만 실행되고,

업데이트될 때는 실행하지 않으려면 함수의 두번째 파라미터로 비어있는 배열을 넣어준다.

 

Info. js - useEffect

 useEffect(() => {
    console.log("마운트될 때만 실행됩니다.");
  }, []);

컴포넌트가 처음 나타날 때만 콘솔에 문구가 나타나고, 그 이후에는 나타나지 않는다.

 

 

8. 2. 2 ) 특정값이 업데이트될 때만 실행하고 싶을 때

 

- 클래스형 컴포넌트

componentDidUpdate(prevProps, prevState) { 
	if (prevProps.value !== this.props.value) { //props의 value 값이 바뀔 때만 작업 수행
    	doSomething( );
    }
}

 

- 함수형 컴포넌트 : useEffect 의 두번째 파라미터로 전달되는 배열 안에 검사하고 싶은 값을 넣어줌

useEffect(()=> {
	console.log(name);
}, [name] );

 

8. 2. 3 ) 뒷정리 함수 

 

useEffect는 기본적으로 렌더링되고 난 직후마다 실행되며, 두 번째 파라미터 배열에 무엇을 넣는지에 따라 실행되는 조건이 다르다. 

컴포넌트가 언마운트되기 전이나 업데이트되기 직전에 어떤 작업을 수행하고 싶으면 뒷정리 함수(return)를 반환해야 한다.

 

App. js

import React, { useState } from "react";
import Info from "./Info";

const App = () => {
  const [visible, setVisible] = useState(false);
  return (
    <div>
      <button
        onClick={() => {
          setVisible(!visible); //visible 값을 toogle
        }}
      >
        {visible ? "숨기기" : "보이기"}
      </button>
      <hr />
      {visible && <Info />}
    </div>
  );
};
export default App;

 

Info. js - useEffect

useEffect(()=>{
	console.log('effect');
    console.log(name);
    return()=>{
    	console.log('cleanup');
        console.log(name);
    };
} /* , []  */  );

 

렌더링될 때마다 뒷정리 함수가 나타나고 업데이트되기 직전의 값을 보여준다.

( 아래 상태에서 숨기기 버튼을 누르면 'cleanup 정서영' 출력 )

 

언마운트될 때만 뒷정리 함수를 호출하고 싶으면 useEffect의 두번째 파라미터에 비어있는 배열을 넣어준다. 

( 보이기 누르면 effect, 숨기기 누르면 cleanup 출력 )

 

8. 3 ) useReducer

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

현재상태, 업데이트를 위해 필요한 정보를 담은 액션 값을 전달받아 새로운 상태를 반환하는 함수. 불변성을 지켜야함

 

1. 리듀서 함수 만들기 function reducer( state, action ) { // action. type에 따라 다른 작업 수행

2. 메인 함수에서 useReducer사용 :  const [ state, dispatch ] = useReducer( reducer, { value: 0 //해당 리듀서의 기본값})

   - dispatch: 액션을 발생시키는 함수, 함수안에 파라미터로 액션을 넣어주면 리듀서 함수가 호출됨.

 

useReducer의 장점: 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다!

 

 

8. 3. 1 ) 카운터 구현하기

 

Counter. js

import React, { useReducer } from "react";

function reducer(state, action) { //1. 리듀서 함수 만들기 
  switch (action.type) {
    case "INCREMENT": //action.type == 'INCREMENT'일때
      return { value: state.value + 1 }; //state의 value에 +1
    case "DECREMENT":
      return { value: state.value - 1 };
    default:
      return state;
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { value: 0 });
  //   현재상태                                     기본값
  //  dispatch: 함수안에 액션값을 파라미터로 넣어주면 리듀서 함수 호출
  return (
    <div>
      <p>
        현재 카운터 값은 <b>{state.value}</b>입니다.
      </p>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button> 
      {/* dispatch에 파라미터로 액션값을 넣어줌 -> reducer함수에서 action type에 맞게 실행 */}
      <button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button>
    </div>
  );
};
export default Counter;

8. 3. 2 ) 인풋 상태 관리하기

 

4장에서 여러개의 input을 받아올 때, e. target. name( input태그의 name 속성 )을 이용한 것과 비슷하게 처리할 수 있다. 

 

Info. js

import React, { useReducer } from "react";

function reducer(state, action) {
  // 리듀서 함수
  return {
    ...state, // 기존값 복사하고
    [action.name]: action.value, // [e.target.name]: e.target.value 와 유사
    // input중 name을 key값으로 써서 입력된 값을 state의 해당 key에 해당하는 곳에 업데이트
  };
}

const Info = () => {
  const [state, dispatch] = useReducer(reducer, {
    name: "",
    nickname: "", //기본값
  });

  const { name, nickname } = state; //객체

  const onChange = (e) => {
    dispatch(e.target); //이벤트 객체가 지니고 있는 e.target값 자체를 액션값으로 사용
    //액션값을 받으면 reducer함수 호출
  };
  return (
    <div>
      <div>
        <input name="name" value={name} onChange={onChange} />
        <input name="nickname" value={nickname} onChange={onChange} />
      </div>
      <div>
        <div>
          <b>이름</b>
          {name}
        </div>
        <div>
          <b>닉네임: </b>
          {nickname}
        </div>
      </div>
    </div>
  );
};

export default Info;

 

8. 4 ) useMemo

함수형 컴포넌트 내부에서 발생하는 연산을 최적화( 특정값이 바뀌었을 때만 연산 )할 수 있다. 

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

// list 값이 바뀌었을 때만 getAverage함수를 호출, 바뀌지 않으면 이전에 연산했던 결과( [list] )를 다시 사용

 

Average. js ( 리스트에 숫자를 추가하면 추가된 숫자들의 평균을 보여주는 컴포넌트 )

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("");
  //list, number (state 2개)

  const onChange = (e) => {
    setNumber(e.target.value); //input들어오면 입력값을 number에 업데이트
  };
  const onInsert = (e) => {
    //버튼 핸들링 메서드
    const nextList = list.concat(parseInt(number)); //number를 추가한 새 배열 만들어서 list 업데이트
    setList(nextList);
    setNumber(""); //number는 초기화
  };
  const avg = useMemo(() => getAverage(list), [list]);
  // list 값이 바뀌었을 때만 getAverage함수를 호출, 아니면 이전에 연산했던 결과( [list] )를 다시 사용
  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <u1>
        {list.map((val, index) => (
          <li key={index}>{val}</li> //list의 원소들을 <li>{원소}<li> 형식으로 만들어줌
        ))}
      </u1>
      <div>
        <b>평균값: </b>
        {avg}
      </div>
    </div>
  );
};

export default Average;

8. 5 )  useCallback

렌더링 성능을 최적화해야 하는 상황에서 사용, 만들어놨던 함수를 재사용할 수 있다. 

위의 Average. js 에서 작성한 코드는 리렌더링 될때마다 새로 만들어진 onChange, onInsert함수를 사용 => 비효율적이므로 useCallback을 사용해서 최적화한다.

const onChange = useCallback(( ) => { 생성하고 싶은 함수 } , [ 이 값이 바뀌었을 때 함수 생성 ] );

// 빈 배열을 넣으면 컴포넌트가 처음 렌더링될 때만 함수 생성

 

Average. js - onChange, onInsert 

const onChange = useCallback((e) => {
    setNumber(e.target.value);
  }, []); //컴포넌트가 처음 렌더링될 때만 함수 생성

  const onInsert = useCallback(() => {
    //버튼 핸들링 메서드
    const nextList = list.concat(parseInt(number)); //number를 추가한 새 배열 만들어서 list 업데이트
    setList(nextList);
    setNumber(""); //number는 초기화
  }, [number, list]); //number 또는 list가 바뀌었을 때만 함수생성 (함수 내부에서 상태값에 의존하므로 필수)

8. 6 ) useRef

함수형 컴포넌트에서 ref를 쉽게 사용할 수 있도록 해준다.

useRef를 사용하여 ref를 설정하면 useRef를 통해 만든 객체 안의 current값이 실제 엘리먼트를 가리킨다.

const Average -> const inputEl = useRef( null ) ; //useRef를 사용해서 ref객체인 inputEl 설정

const Average -> const onInsert = useCallback(( ) =>{ ... inputEl. current. focus( ); //포커스 넘겨주기 }, [ number, list ] );

return -> <input value={number} onChange={onChange} ref={ inputEl } /> // inputEl는 input요소를 카리킨다 

 

8. 6. 1 ) 로컬변수 사용하기

 

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

 

클래스형 컴포넌트

import React, { Component } from "react";
// 렌더링과 상관없이 이 컴포넌트만의 로컬변수 사용 가능
class MyComponent extends Component {
  id = 1; //로컬변수
  setId = (n) => {
    this.id = n;//로컬변수에 접근하는 함수
  };
  printId = () => {
    console.log(this.id);
  };
  render() {
    return <div>MyComponent</div>; //자기자신을 리턴
  }
}
export default MyComponent;

함수형 컴포넌트 - 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;

 

useRef로 감싸진 current가 가리키는 값은 React에 의해 기억되기 때문에 직접 변경하기 전까지 해당 컴포넌트가 호출될 때마다 동일하다.

8. 7 ) 커스텀 Hooks 만들기

여러 컴포넌트에서 비슷한 기능을 공유할 경우, 이를 나만의 Hook으로 작성하여 로직을 재사용할 수 있다.

 

기존에 Info 컴포넌트에서 여러개의 인풋을 관리하기 위해 useReducer로 작성했던 로직을 useInputs라는 Hook으로 따로 분리해 보자.

// 보통 커스텀 Hook은 use라는 키워드로 시작하는 파일을 많이 쓴다.

 

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);
  //dispatch -> reducer함수 호출
  const onChange = (e) => {
    dispatch(e.target); //이벤트 객체가 지니고있는 e.target 값 자체를 액션 값으로 사용
  };
  return [state, onChange];
}

Info. js

import React from "react";
import useInputs from "./useInputs";
/*
function reducer(state, action) {
  // 리듀서 함수
  return {
    ...state, // 기존값 복사하고
    [action.name]: action.value, // [e.target.name]: e.target.value 와 유사
    // input중 name을 key값으로 써서 입력된 값을 state의 해당 key에 해당하는 곳에 업데이트
  };
}*/
const Info = () => {
  const [state, onChange] = useInputs({
    name: "",
    nickname: "", //기본값
  });

  const { name, nickname } = state; //객체
  /*
  const onChange = (e) => {
    dispatch(e.target); //이벤트 객체가 지니고 있는 e.target값 자체를 액션값으로 사용
    //액션값을 받으면 reducer함수 호출
  }; */
  return (
    <div>
      <div>
        <input name="name" value={name} onChange={onChange} />
        <input name="nickname" value={nickname} onChange={onChange} />
      </div>
      ...
    </div>
  );
};

export default Info;

 

이번 장에서는 다양한 Hooks에 대해 알아보았다. 

  • 가변적인 상태를 가질 수 있게 해주는 useState
  • 리액트가 렌더링될 때마다 특정 작업을 수행하는 useEffect
  • useState와 비슷한 기능을 하지만 업데이트를 위한 액션값을 전달받아 새로운 상태를 반환하는 useReducer
  • 특정값이 바뀌었을 때만 연산하는 useMemo
  • 만들어놨던 함수를 재사용하는 useCallback
  • ref를 설정하거나 로컬변수를 만들어주는 useRef
  • 커스텀 Hook만들기

 

리액트에서는 함수형 컴포넌트를 쓰는 것을 권장하지만 기존에 클래스형 컴포넌트로 작성된 파일을 유지, 보수할 때 굳이 함수형 컴포넌트와  Hooks를 사용하는 형태로 전환할 필요는 없다.

 


▶ useReducer vs useState - 뭐 쓸까?

어떨 때 useReducer 를 쓰고 어떨 때 useState 를 써야 할까요?
일단, 여기에 있어서는 정해진 답은 없습니다. 상황에 따라 불편할때도 있고 편할 때도 있습니다.
예를 들어서 컴포넌트에서 관리하는 값이 딱 하나고, 그 값이 단순한
숫자, 문자열 또는 boolean 값이라면 확실히 useState 로 관리하는게 편할 것입니다.
const [value, setValue] = useState(true);
하지만, 만약에 컴포넌트에서 관리하는 값이 여러개가 되어서 상태의 구조가 복잡해진다면 useReducer로 관리하는 것이 편해질 수도 있습니다.
이에 대한 결정은, 앞으로 여러분들이 useState, useReducer 를 자주 사용해보시고 맘에드는 방식을 선택하세요.
저의 경우에는 setter 를 한 함수에서 여러번 사용해야 하는 일이 발생한다면
setUsers(users => users.concat(user)); setInputs({ username: '', email: '' });
그 때부터 useReducer 를 쓸까? 에 대한 고민을 시작합니다. useReducer 를 썼을때 편해질 것 같으면 useReducer 를 쓰고, 딱히 그럴것같지 않으면 useState 를 유지하면 되지요.