본문 바로가기
프로그래밍 개발/프론트엔드 개발자 기본 지식

React Hooks

by Jinseok Kim 2022. 5. 16.
반응형

React Hooks

 

  • React 에서 기존에 사용하던 Class를 이용한 코드를 작성할 필요 없이, state와, 여러 React 기능을 사용할 수 있도록 만든 라이브러리입니다. 
  • 함수 컴포넌트도 클래스 컴포넌트처럼 사용할 수 있으며 함수 컴포넌트는 클래스 컴포넌트와 다르게, 모듈로 활용하기가 쉬우므로 서로의 장점을 전부다 가지고 있습니다.

 

 

Hooks 간단 소개

 

1. useState: 컴포넌트의 state(상태)를 관리 할 수 있습니다,

 

2. useEffect: 어떤 변수가 변경될때마다, 특정기능이 작동하도록 할 수 있습니다.

 

3. useContext: 여러개의 컴포넌트에서 사용할 수 있는, 변수 or 함수를 만들 수 있습니다. ( 전역으로 관리 )

 

4. useReducer: state(상태) 업데이트 로직을 reducer 함수에 따로 분리 할 수 있습니다.

 

5. useRef: 컴포넌트나 HTML 요소를 래퍼런스로 관리할 수 있습니다.

 

6. useImperativeHandle: useRef로 만든 래퍼런스를 상위 컴포넌트로 전달할 수 있습니다.

 

7. useMemo, useCallback: 의존성 배열에 적힌 값이 변할 때만 값,함수를 다시 정의할 수 있습니다.

 

8. useLayoutEffect: 모든 DOM 변경 후 브라우저가 화면을 그리기(render)전에 실행되는 기능을 정할 수 있습니다.

 

 

 

 

useState

 

useEffect(function, deps)

 

  • function : 실행하고자 하는 함수
  • deps : 배열 형태. function을 실행시킬 조건.
    deps에 특정값을 넣게 되면 컴포넌트가 mount 될 때, 지정한 값이 업데이트될 때 useEffect를 실행합니다.

 

// React에 기본적으로 내장되어 있는 useState 훅을 사용하면, state를 만들 수 있습니다.

import { useState } from "react";


// const [state, state변경함수] = useState(기본 state값);

const [isLogin, setIsLogin] = useState(false);


// "setIsLoging"으로 state의 값을 true로 변경합니다.

setIsLogin(true);

 

  • state는 어디에서든지 사용가능합니다. 하지만 웹, 앱이 커질수록 컴포넌트가 많아지고 자식으로 내려야할 props가 점점 많아지므로 점점 상태관리가 어려워지는 단점이 있습니다.

 

 

 

 

 

useEffect

 

 

 

useEffect(function, deps)

 

  • function : 실행하고자 하는 함수
  • deps : 배열 형태. function을 실행시킬 조건입니다.
    deps에 특정값을 넣게 되면 컴포넌트가 mount 될 때, 지정한 값이 업데이트될 때 useEffect를 실행합니다.

 

 

// React에 기본적으로 내장되어 있는 useState와, useEffect 불러오기

import { setState, useEffect } from "react";

...

function App() {
  const [data, changeData] = setState(false)
  
  // useEffect(실행할 함수, 트리거가 될 변수)
  
  useEffect(() => {
    if (data.me === null) {
      console.log("Data changed")
    }
  }, [data]);  
  
  // data변수가 바뀔때마다, react가 이를 감지해, 콘솔창에 "Data changed" 출력
  
  return (
    <div>
      <button value="적용" onClick={changeData(!data)} />
    </div>
  )
}

export default App;

 

  • useEffect() 함수는 React component가 렌더링 될 때마다 특정 작업(Sied effect)을 실행할 수 있도록 하는 리액트 Hook입니다. 여기서 Side effect는 component가 렌더링 된 이후에 비동기로 처리되어야 하는 부수적인 효과들을 뜻합니다.
  • 이러한 기능으로 인해 함수형 컴포넌트에서도 클래스형 컴포넌트에서 사용했던 생명주기 메서드를 사용할 수 있게 되었습니다.

 

 

 

 

 

useContext

 

 

  • React에서 context 없이 변수나 함수를 다른 컴포넌트로 전달하려면 부모자식간에만 전달이 가능하므로, 컴포넌트가 많아질수록 불필요한 컴포넌트를 많이 거쳐야하는 문제가 발생합니다.
  • 하지만 context를 이용하면, 중간과정을 재치고 전역으로 전달할 수 있게되었습니다.

 

 

새로운 context 만들기

 

// newContext.js

import { createContext } from "react"  // createContext 함수 불러오기

// context안에 homeText란 변수를 만들고, 공백("") 문자를 저장한다.
const newContext = createContext({
  homeText: "",
})

 

 

 

context를 사용할 부분 선택하기, 새로운 정보 저장하기

 

// App.js

import React from "react";
import { View } from "react-native";
import Home from "./Home"; // 자식 컴포넌트 불러오기

import { newContext } from "./newContext"; // context 불러오기


export default function App() {

  // context에 저장할 정보를 입력한다.
  const homeText = "is Worked!"

  // NewContext.Provider로 우리가 만든 context를 사용할 부분을 감싸준다.
  return (
        <newContext.Provider value={{ homeText }}>
          <View>
            <Home />
          </View>
        </newContext.Provider>
  );
}

 

 

 

 

context 사용하기

 

// Home.js

import React from "react";
import { Text, View } from "react-native";

import { useContext } from "react";
import { newContext } from "../newContext";


export default function Home() {

  // useContext hook 사용해서, newContext에 저장된 정보 가져오기
  const { homeText } = useContext(newContext);
  
  // 불러온 정보 사용하기!!
  return (
    <View>
      <Text>{homeText}<Text>
    </View>
  );
}

 

  • props가 2중 3중으로 넘겨야되는 상황에서 context는 매우 유용한 선택입니다.
  • 전역으로 필요한 함수나 로그인 세션 데이터 혹은 유저 데이터를 context에 담아 놓으면 프로젝트 전역 어디서나 props로 넘기지 않아도 쓸 수 있는 편리함을 제공합니다.

 

 

 

 

 

useReducer

 

  • useReducer는 State(상태)를 관리하고 업데이트하는 Hook인 useState를 대체할 수 있는 Hook입니다. 다시 말해, useReducer는 useState처럼 State를 관리하고 업데이트 할 수 있는 Hook입니다.
  • useReducer의 묘미는, 컴포넌트 내에서 State를 업데이트하는 로직 부분을 그 컴포넌트로부터 분리시키는 것을 가능하게 해준다는 것입니다.
  • useReducer는 State 업데이트 로직을 분리하여 컴포넌트의 외부에 작성하는 것을 가능하게 함으로써, 코드의 최적화를 이루게 해줍니다.

 

 

 

 

 

- useState 과 useReducer 비교

 

 

  • useState
    • 관리해야 할 State가 1개일 경우
    • 그 State가 단순한 숫자, 문자열 또는 Boolean 값일 경우
  • useReducer
    • 관리해야 할 State가 1개 이상, 복수일 경우
    • 혹은 현재는 단일 State 값만 관리하지만, 추후 유동적일 가능성이 있는 경우
    • 스케일이 큰 프로젝트의 경우
    • State의 구조가 복잡해질 것으로 보이는 경우

 

 

 

const [state, dispatch] = useReducer(reducer, initialState, init);

 

  • state: 컴포넌트에서 사용할 State(상태).
  • dispatch: reducer 함수를 실행시키며, 컴포넌트 내에서 state의 업데이트를 일으키기 위해서 사용하는 함수입니다. 
  • reducer: 컴포넌트 외부에서 state를 업데이트하는 로직을 담당하는 함수. 현재의 state와 action 객체를 인자로 받아서, 기존의 state를 대체(replace)할 새로운 State를 반환(return)하는 함수입니다.
  • initialState: 초기 State
  • init: 초기 함수

 

 

 

import React, { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "decrement":
      // action의 type이 "decrement"일 때, 현재 state 객체의 count에서 1을 뺀 값을 반환함
      return { count: state.count - 1 };
    case "increment":
      // action의 type이 "increment"일 때, 현재 state 객체의 count에서 1을 더한 값을 반환함
      return { count: state.count + 1 };
    default:
      // 정의되지 않은 action type이 넘어왔을 때는 에러를 발생시킴
      throw new Error("Unsupported action type:", action.type);
  }
}

function Counter() {
  const [number, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <>
      {/* 현재 카운트 값은 state인 number 객체의 count로부터 읽어옴 */}
      <h1>Count: {number.count}</h1>
      {/* 카운트 값의 변경을 위해 각 버튼이 클릭되면 dispatch 함수가 발동되면서 reducer 함수가 실행됨.
          dispatch 함수의 인자로, action 객체가 설정되었는데,
          action 객체의 type에는 어떤 버튼을 클릭하였는지에 따라
          "decrement" 또는 "increment"가 들어감
      */}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}

export default Counter;

 

 

 

 

 

 

 

 

useRef

 

 

  • useRef 함수는 current 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current 속성에 할당합니다. 이 current 속성은 값을 변경해도 상태를 변경할 때 처럼 React 컴포넌트가 다시 랜더링되지 않습니다.
  • React 컴포넌트가 다시 랜더링될 때도 마찬가지로 이 current 속성의 값이 유실되지 않습니다.
  • useRef 함수를 사용하는 또 다른 경우가 있는데, DOM 노드나 React 엘리먼트에 직접 접근하기 위해서 입니다.

 

 

 

 

useState 이용한 카운트

 

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

function ManualCounter() {
  const [count, setCount] = useState(0);

  let intervalId;

  const startCounter = () => {
    // 💥 매번 새로운 값 할당
    intervalId = setInterval(() => setCount((count) => count + 1), 1000);
  };
  const stopCounter = () => {
    clearInterval(intervalId);
  };

  return (
    <>
      <p>자동 카운트: {count}</p>
      <button onClick={startCounter}>시작</button>
      <button onClick={stopCounter}>정지</button>
    </>
  );
}

 

  • 위의 코드에서 가장 큰 걸릿돌은 안에서 선언된 intervalId 변수를 startCounter() 함수와 stopCounter() 함수가 공유할 수 있도록 해줘야 한다는 것입니다.
  • 그럴려면 intervalId 변수를 두 함수 밖에서 선언해야하는데 그럴 경우, count 상태값이 바뀔 때 마다 컴포넌트 함수가 호출되어 intervalId도 매번 새로운 값으로 바뀔 것입니다. 따라서, 브라우저 메모리에는 미처 정리되지 못한 intervalId 들이 1초에 하나식 쌓여나갈 것입니다. 

 

 

 

 

useRef 이용한 카운트

 

import React, { useState, useRef } from "react";

function ManualCounter() {
  const [count, setCount] = useState(0);
  const intervalId = useRef(null);
  console.log(`랜더링... count: ${count}`);

  const startCounter = () => {
  	// ref의 current에 새로운 intervalId 시작하기
    intervalId.current = setInterval(
      () => setCount((count) => count + 1),
      1000
    );
    console.log(`시작... intervalId: ${intervalId.current}`);
  };

  const stopCounter = () => {
  	// clearInterval 함수로 intervalId 정리
    clearInterval(intervalId.current);
    console.log(`정지... intervalId: ${intervalId.current}`);
  };

  return (
    <>
      <p>자동 카운트: {count}</p>
      <button onClick={startCounter}>시작</button>
      <button onClick={stopCounter}>정지</button>
    </>
  );
}

//랜더링... count: 0
//시작... intervalId: 17
//랜더링... count: 1
//랜더링... count: 2
//랜더링... count: 3
//랜더링... count: 4
//랜더링... count: 5
//정지... intervalId: 17
//시작... intervalId: 32
//랜더링... count: 6
//랜더링... count: 7
//랜더링... count: 8
//정지... intervalId: 32

 

  • 시작 버튼을 누르면 새로운 intervalId가 생성되고, 정지 버튼을 누르면 기존 intervalId가 정리되는 것을 확인할 수 있습니다.

 

 

 

 

 

 

 

 

 

useImperativeHandle

 

 

  • React hook 중에 useImperativeHandle 이라는 훅이 있습니다. 이 훅은 forwardRef 를 사용해서 ref를 사용하는 부모 측에서 커스터마이징된 메서드를 사용할 수 있게 해줍니다.
  • 리액트에서는 데이터가 부모에서 자식으로만 흐르는데, 이렇게 단방향으로만 작동하는 방식을 회피해야 할 때는 Redux나 Context API 같은 것을 사용합니다. 이런 것들을 사용하면 상태를 끌어올리지 않고도 부모에게 변경된 데이터가 적용되게 할 수 있습니다.
  • 하지만 굳이 Redux나 Context API를 사용할 만큼의 일이 아닐 때 useImperativeHandle을 사용할 수도 있으므로 상태나 로직들은 자식 컴포넌트가 갖고 있고, 부모 컴포넌트는 ref.current에서 필요한 프로퍼티를 가져오기만 하면 됩니다.

 

 

function ParentComponent() {
  // inputRef라는 동아줄을 만들어서 자식에게 보낸다
  const inputRef = useRef();
  return (
    <>
          <FancyInput ref={inputRef} />
          <button onClick={() => inputRef.current.realllyFocus()}>포커스</button>
    </>
  )
}

 

function FancyInput(props, ref) {
  // 부모가 내려준 동아줄 ref에다가 이것 저것 작업을 한다
  // 부모는 이 로직에 대해 모르고, 위로 끌어올리지 않고도 그냥 ref.current로 접근하여 사용만 하면 된다
  useImperativeHandle(ref, () => ({
    reallyFocus: () => {
      ref.current.focus();
      console.log('Being focused!')
    }
  }));
  // ref는 input을 참조하고 있다. 
  return <input ref={inputRef} />
}
 
// ref를 컴포넌트에 달 때는 forwardRef로 감싼다
FancyInput = forwardRef(FancyInput)

 

  • useImperativeHandle 의 첫 번째 인자로는 프로퍼티를 부여할 ref이고, 두 번째 인자는 객체를 리턴하는 함수이다. 이 객체에 추가하고 싶은 프로퍼티를 정의하면 됩니다.
  • 예시 코드에서는 reallyFocus 라는 메서드를 정의했고, 이 메서드를 호출하기 위해서는 사용처에서는 그 ref.current에 정의된 메서드를 호출하기만 하면 된다.
  • 이렇게 할 때의 장점은 child component에 상태나 로직을 isolate할 수 있다는 점입니다.

 

 

 

 

 

 

 

 

 

useMemo & useCallback

 

 

  • useMemo와 useCallback은 매우 유사합니다. 내부에서 발생하는 연산을 최적화하며 특정 값이 바뀌었을 때만 연산을 실행하고, 바뀌지 않았으면 이전에 연산했던  결과를 다시 사용하는 방식입니다.
  • useMemo 는 값을 반환하고 useCallback은 함수를 반환합니다.
  • 즉 컴포넌트가 랜더링 하였을때 useMemo와 useCallback로 감싼 함수와 값들은 메모제이션용 메모리에 저장해 놓기 때문에 다시 생성되지 않아 성능에 최적화를 제공합니다. 하지만 메모제이션용 메모리가 필요하므로 useMemo와 useCallback을 남용하면 안됩니다.


 

 

 

- useMemo와 useCallback을 쓰지 않을 경우

 

import React, { useState } from 'react'; 


const Average = () => { 

const [list, setList] = useState([]); 

const [number, setNumber] = useState(''); 

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


const onChange = (e) => { 
    setNumber(e.target.value); 
}; 


const onInsert = () => { 
    const nextList = list.concat(parseInt(number)); 
    setList(nextList); setNumber(''); 
}; 


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> {getAverage()} </div> 
</div> ); 

};



export default Average;

 

  • 글자 입력 시에도 getAverage함수가 동작하기 때문에 콘솔창에 "평균값 계산중... "이 뜸니다.
  • 버튼 클릭시에도 getAverage함수가 동작하기 때문에 콘솔창에 "평균값 계산중..."이 뜸니다.
  • 종합적으로, getAverage함수가 필요하지 않은 동작에서도 작동이 됩니다.

 

 

 

 

 

- useMemo

 

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

const Average = () => { 

const [list, setList] = useState([]); 
const [number, setNumber] = useState('');

const getAverage = useMemo(() => { 
    console.log('평균값 계산 중...'); 
    if (list.length === 0) return 0; const sum = list.reduce((a, b) => a + b); 
    return sum / list.length; 
}, [list]); //list값이 업데이트 될때만 실행 

const onChange = (e) => { setNumber(e.target.value); };

const onInsert = () => { 
const nextList = list.concat(parseInt(number)); 
setList(nextList); setNumber(''); 
}; 


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> {getAverage} {/* useMemo는 값을 반환 */} 
	</div>
</div> 
	); 
};

 

  • 첫 렌더링과 값을 입력 할때만 렌더링되는 코드입니다, 즉 [ ] 안에 써준 값 list가 업데이트 될 때만 렌더링이 돌아갑니다.

 

 

 

 

 

 

- useCallback

 

import React, { useState, useCallback, 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 = useCallback((e) => { 
setNumber(e.target.value); }, []); 

const onInsert = useCallback(() => { 
const nextList = list.concat(parseInt(number)); setList(nextList); setNumber(''); 
}, [number, 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;

 

  • onChange와 onInsert함수에 useCallback을 감싸므로,  [ ] <- 괄호 안의 값이 바뀌었을 때만 함수를 생성해 줍니다.
  • 하지만 useCallback을 사용하지 않은 경우 리렌더링 될때마다 함수를 새로 만들어 줍니다.

 

 

 

 

 

 

 

 

useLayoutEffect

 

 

 

- useLayoutEffect와 useEffect 비교

 

  • useLayoutEffect는 컴포넌트들이 render 된 후 실행되며, 그 이후에 paint 가 됩니다. 이 작업은 동기적(synchronous) 으로 실행되기 때문에 paint 가 되기전에 실행되기 때문에 dom 을 조작하는 코드가 존재하더라도 사용자는 깜빡임을 경험하지 않습니다
  • 반대로 useEffect 는 컴포넌트들이 render 와 paint 된 후 실행됩니다. 비동기적(asynchronous) 으로 실행됩니다. paint 된 후 실행되기 때문에, useEffect 내부에 dom 에 영향을 주는 코드가 있을 경우 사용자 입장에서는 화면의 깜빡임을 보게됩니다..

 

 

 

 

- useLayoutEffect을 사용할 때

 

  • useLayoutEffect 는 동기적으로 실행되고 내부의 코드가 모두 실행된 후 painting 작업을 거칩니다.
  • 따라서 로직이 복잡할 경우 사용자가 레이아웃을 보는데까지 시간이 오래걸린다는 단점이 있어, 기본적으로는 항상 useEffect 만을 사용하는 것을 권장합니다. 구체적인 예시로는

 

  • 데이터 fetch
  • event handler
  • state reset

 

 

등의 작업은 항상 useEffect 를 사용하되,

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

  useLayoutEffect(() => {
    if (value === 0) {
      setValue(10 + Math.random() * 200);
    }
  }, [value]);

  console.log('render', value);

  return (
    <button onClick={() => setValue(0)}>
      value: {value}
    </button>
  );
};

 

  • 위와 같이 화면이 깜빡거리는 상황은 위 코드의 state가 존재하며, 해당 state 이 조건에 따라 첫 painting 시 다르게 렌더링 되어야 할 때는 useEffect 사용 시 처음에 0이 보여지고 이후에 re-rendering 되며 화면이 깜빡거려지기 때문에 useLayoutEffect 를 사용합니다.
반응형

댓글