[React] Hooks 정리

·2023년 4월 8일
0
post-thumbnail

Hooks 이란

Hook은 React 버전 16.8부터 React 요소로 새로 추가된 기능이다. 이전에 리액트가 겪던 문제들을 해결해주며,

  • 컴포넌트 사이에서 상태 로직을 재사용하기 어려움
  • 복잡한 컴포넌트들은 이해하기 어려움
  • Class 사용으로 인한 혼란 등

기존 함수 컴포넌트에서 할 수 없었던 다양한 작업을 할 수 있게 도와준다.
예) 클래스형 컴포넌트를 사용하지 않고도 함수 컴포넌트에서 상태 값 및 자식 요소에 접근 가능!

참고: React - Hook의 개요


1. useState

개념

  • useState는 state와 setState를 배열 형태로 리턴해 준다.
  • state는 현재 상태 값이고, setState를 통해 상태 변경이 가능하다.
  • setState를 사용해 상태를 변경할 때마다 컴포넌트는 다시 렌더링이 된다.

사용방법

생성하기

const [state, setState] = useState(초기값);

state는 원하는 변수명으로 작성하면 된다.

// 사용 예)
const [number, setNumber] = useState(0);

상태 변경하기

// 상태 변경 방법 1
setState(newState);

// 상태 변경 방법 2
setState((prevState) => {
	// .. some works ...
	return newState; // return [input, ...prevState];
});
  • 상태 변경 방법 1: 일반적인 방법
  • 상태 변경 방법 2: 새로 변경될 state 값이 이전 state와 연관이 있을 경우 setState의 인자로 새로운 state를 리턴하는 콜백함수를 넣어주는 것이 좋음

콜백함수: 다른 함수의 인자로 전달된 함수

초기 렌더링만 가능하도록 !

useState(() => {
	return heavyWorks()
});

리액트는 state가 변경될 때마다 렌더링이 된다.
만일 useState를 사용해서 초기값을 받아올 때 무거운 일을 해야 한다면, useState 인자로 콜백함수를 넣어줌으로써 맨 처음 렌더링 될 때만 실행되도록 할 수 있다.


2. useEffect

개념

어떤 컴포넌트가 마운트(화면에 첫 렌더링), 업데이트(다시 렌더링), 언마운트(화면에서 사라짐)되었을 때 실행시켜주고 싶은 작업이 있을 경우 사용한다.

사용방법

(1) 렌더링 될 때마다 매번 콜백함수 실행

  • 인자로 콜백함수만 받음
useEffect(() => {
  // 작업
});

(2) 마운트(첫 렌더링) 될 때 실행

  • 인자로 콜백함수, 빈 배열(dependency array, 의존성 배열)을 받음
useEffect(() => {
  // 작업
}, []);

(3) 마운트 될 때, [value] 값이 바뀔 때 실행

  • 인자로 콜백함수, 배열(dependency array, 의존성 배열)을 받음
useEffect(() => {
  // 작업
}, [value]);

(4) 정리 작업 (Clean Up)

  • 해당 컴포넌트가 언마운트 될 때(화면에서 사라질 때) return 코드 실행
  • 타이머를 멈추거나, 등록한 리스너를 정리하는 등의 작업을 할 때 사용
useEffect(() => {
  // 구독
  return () => {
    // 구독 해지
  }
}, []);

⚡️ 마운트와 렌더의 차이 확인하기


3. useRef

개념

변수 관리 또는 DOM 요소에 접근할 때 사용한다.

(1) 변수관리(저장공간)

const ref = useRef(초기값); // { ref.current: 초기값}
ref.current = '변경할 값'; // { ref.current: 변경할 값}
  • 변화는 감지하지만, 그 변화가 렌더링을 발생시키지 않는 값을 다룰 때 사용한다.
  • useRef 값은 전생애주기(컴포넌트가 마운트 ~ 언마운트되기 전까지)를 통해 유지된다.

state 와의 차이

  • state의 변화 > 렌더링 > 컴포넌트 내부 변수들이 초기화 됨
    👉🏼 원하지 않는 렌더링 때문에 곤란해짐
  • ref의 변화 > 렌더링 X > 컴포넌트 내부 변수들의 값이 유지됨
    👉🏼 불필요한 렌더링을 막을 수 있음

단순 변수와의 차이

  • 단순 변수 : 렌더링 시 초기값으로 값이 초기화 됨
  • ref : 렌더링 되어도 값이 유지됨

(2) DOM 요소 접근

const ref = useRef();
<input ref={ref} />

대표적인 예시로는 input 요소에 focus()를 주는 것이 있다.
순수 자바스크립트의 Document.querySelector() 와 유사하다.

사용 방법

(1) 변수 관리 예제: 화면에 렌더링 되는 수 확인하기

useState로 렌더링 수를 확인하려고 한다면 무한루프에 빠지게 된다.
count 증가 시 업데이트 되어 useEffect가 불리고, useEffect 실행 시 renderCount가 증가되어 다시 useEffect가 불린다.

// useState
const [count, setCount] = useState(1);
const [renderCount, setRenderCount] = useState(1);
useEffect(() => {
  console.log(‘렌더링’);
  setRenderCount(renderCount + 1);
});

useRef를 사용하면 해결할 수 있다. 렌더링 수를 증가시켜도 리렌더링이 되지 않는다.

// useRef
const [count, setCount] = useState(1);
const renderCount = useRef(1);
useEffect(() => {
  renderCount.current = reundercount.current +1 ;
});

(2) DOM 요소 접근 예제: input에 focus 주기

const inputRef = useRef();
useEffect(() => {
  inputRef.current.focus();
}, []);

<input ref={inputRef} type=“text” placeholder=“username”/>

4. useContext

개념

전역적으로 사용하고 싶은 값이 있을 때 사용한다. 예시로 로그인된 사용자 정보, 테마, 언어 등이 있다.

🤬 모든 정보를 props로 일일이 전달할 경우 문제점
가장 하위에 있는 컴포넌트들만 해당 값이 필요함에도 불구하고 중간 컴포넌트들을 거쳐야 해(=Prop Drilling), 다음과 같은 문제들이 발생할 수 있다. 그리고 문제가 발생한 경우 부모 컴포넌트를 하나하나 확인하며 오류를 해결해야 한다.

  • 전달해야 하는 props의 양이 많아진다.
  • 코드가 지저분해 질 수 있다.
  • 이상한 데이터를 전달할 수 있다.
  • 수정한 데이터를 전달할 수 있다.

사용방법

  • 사용 예시 컴포넌트 구조
    예시 컴포넌트 구조

step 1. React.createContext()를 통해 context를 만든다.

import { createContext } from ‘react’;
export const TempContext = React.createContext('초기값');

step 2. 상위 컴포넌트에서 context 사용이 필요한 컴포넌트들을 감싼다.

// App.js
// App 컴포넌트에서 context로 Page 컴포넌트를 감싼다.
// 여기서 context로 하위 컴포넌트를 감싸지 않을 경우
// 하위 컴포넌트에서 context를 호출하면 '초기값'이 불려진다.
const App = () => {
  return (
	<TempContext.Provider value={value}>
	  <Page />
	</TempContext.Provider>
  )
};

// Page.js
const Page = () => {
  return (
	<div className="page">
 	  <Header />
 	  <Content />
      <Footer />
 	</div>
  );
};

step 3. context를 사용할 컴포넌트에서 useContext를 이용해 값을 가져온다.

// Header.js
const Header = () => {
  const { value } = useContext(TempContext);
  return (...생략...);
}

주의할 점

context를 사용하면 컴포넌트를 재사용하기가 어려워지므로 꼭 필요할 때만 사용해야 한다.


5. useMemo

개념

  • 컴포넌트 성능 최적화를 위한 Hook
  • useMemo에서 말하는 Memo는 Memoization을 뜻한다.
  • Memoization: 동일한 값을 리턴해주는 함수를 반복적으로 호출해야 한다면, 맨 처음 값을 계산할 때 해당 값을 메모리에 저장한 후 다시 계산하지 않고 메모리에서 꺼내서 사용하는 것이다.
    (= 캐싱해둔 뒤, 캐시에서 꺼내 사용하는 것)

🤯 useMemo는 값을 재활용하기 위해 메모리를 소비하며 값을 저장해 두는 것이다. 따라서 무분별하게 사용할 경우 성능이 악화될 수 있으므로 필요할 때만 사용해야 한다.

사용방법

// const value = useMemo(콜백함수, 의존성 배열);
const value = useMemo(() => {
  return 함수();
}, [item]);

첫번째 인자에는 콜백함수를 넣고, 두번째 인자에는 의존성 배열을 넣는다.

  • 콜백함수: Memoization을 해줄 값을 계산해서 리턴해주는 함수이다.
  • 의존성 배열: 요소의 값이 업데이트 될 때만 콜백함수를 다시 호출한다.
    • 다시 호출되는 경우, Memoization 한 값을 업데이트해서 다시 Memoization을 해준다.
    • 빈 배열을 전달한다면 맨 처음 컴포넌트가 마운트 되었을 때만 값을 계산하고, 이후에는 Memoization된 값을 꺼내와서 사용한다.

빛을 발하는 순간!

useEffect()를 사용하는데 비교해야 할 값이 객체 타입인 경우 useMemo()가 유용하다.

useMemo() 사용 전

// 이 경우 렌더링 된다면 location이 바뀌지 않아도 console.log() 는 계속 찍힌다.
const [isKorea, setIsKorea] = useState(true);
const location = {
  country: isKorea ? '한국' : '외국'
};
useEffect(() => {
  console.log('useEffect() 호출');
}, [location]);

useMemo() 사용해야 하는 이유

원시(Primitive) 타입객체(Object) 타입
String, Number, Boolean, Null, Undefined, BigInt, Symbol원시타입을 제외한 모든 것 Object, Array, … 등

변수에 객체 타입의 값을 할당하면, 객체는 너무 크기 때문에 메모리상의 어떤 공간에 보관되고 변수에는 객체가 담긴 메모리의 주소가 할당된다.

  • 원시 타입 간의 비교
const one = “Hello”;
const two = “Hello”;
console.log(one === two); // true
  • 객체 타입 간의 비교
const one = { obj: Hello };
const two = { obj: Hello };
console.log(one === two); // false
// 변수에 메모리 주소가 들어있고, 두 객체에는 각각 다른 주소가 들어있음

해당 컴포넌트가 다시 호출되면 변수도 객체를 또 할당받고 다른 메모리에 저장 된다. 이로 인해 변수 속 메모리 주소 값도 바뀌게 되어 useEffect()가 계속 실행된다.

useMemo() 적용

const [isKorea, setIsKorea] = useState(true);
const location = useMemo(() => {
  return {
    country: isKorea ? '한국' : '외국';
  };
}, [isKorea]);

useEffect(() => {
  console.log('useEffect() 호출');
}, [location]);

6. useCallback

개념

  • 컴포넌트 성능 최적화를 위한 Hook
  • useMemo와 마찬가지로 Memoization 해준다.
  • 차이가 있다면, useMemo는 을 캐싱해두는 것이고,
    useCallback은 콜백 함수 그 자체를 캐싱해둔다.

사용방법

// 일반 함수
const calculate = (num) => {
  return num + 1;
};

// useCallback
// const value = useCallback(콜백함수, 의존성 배열);
const calculate = useCallback((num) => {
  return num + 1;
}, [item]);

함수형 컴포넌트는 함수이다. 렌더링 시 컴포넌트 함수가 호출되며, 이후에 모든 내부 변수는 초기화가 된다. 그렇기 때문에 렌더링이 될 때마다 함수 객체를 할당받게 된다.

useCallback을 사용하면 Memoize 된 함수를 재사용하기 때문에 렌더링 될 때마다 함수 객체를 할당받는 행위를 막을 수 있다. 단, 의존성 배열 내부에 명시해준 값이 변경되면 새로 만들어진 함수 객체로 초기화 된다.


7. useReducer

개념

useState 처럼 state 생성, 관리를 위한 Hook이다. 복잡한 state를 다뤄야 할 때 사용한다.

  • reducer: state를 업데이트 하는 역할로, 컴포넌트의 상태를 변경하고 싶다면 reducer가 호출되어야 한다.
  • dispatch: state 업데이트를 위한 요구
  • action: 요구의 내용
const reducer = (state, action) => {
  // 수행할 일
};
const [item, dispatch] = useReducer(reducer, '초기값');

useReducer는 useState 처럼 배열을 반환해 준다. 배열의 첫번째 요소(item)는 새로 생성한 state이고, 배열의 두번째 요소(dispatch)에는 useReducer가 만들어준 dispatch가 들어있다. useReducer는 인자로 리듀서와 초기 값을 받는다.

사용방법

새로 생성한 item의 값을 수정하고 싶다면 reducer를 통해서만 가능하다. 수정을 원할 때마다 dispatch를 호출해야 하고, dispatch를 부르면 reducer가 호출된다. dispatch의 인자로 action을 전달할 수 있다.

// 예제) 버튼을 클릭할 때마다 숫자가 1씩 증가 또는 감소

// 1. 컴포넌트 내부에 useReducer를 선언한다.
const [number, dispatch] = useReducer(reducer, 0);

// 2. 컴포넌트 밖에 reducer를 만든다.
// 추후 편리한 수정을 위해 ACTION_TYPES를 따로 빼주는 것이 좋다.
const ACTION_TYPES = {
  increase: 'increase',
  decrease: 'decrease',
};
const reducer = (state, action) => {
  // state는 number 상태이다.
  switch(action.type) {
    case ACTION_TYPES.increase:
      return state + 1;
    case ACTION_TYPES.decrease:
      return state - 1;
    default:
      return state;
  };
};

// type: 속성, payload: 해당 행동과 관련된 데이터
<button onClick={() => dispatch({ type: ACTION_TYPES.increase, payload: 1 })}>증가</button>
<button onClick={() => dispatch({ type: ACTION_TYPES.decrease, payload: 1 })}>감소</button>

dispatch()는 useReducer가 만들어준 함수이고, 인자로 action을 넣어주면 된다. dispatch를 호출하면 reducer가 호출되고, 인자로 action이 전달되어 전달된 내용을 토대로 state가 변경된다.


8. React.memo

개념

  • useMemo, useCallback 처럼 컴포넌트 성능 최적화를 위한 Hook으로 불필요한 렌더링을 막을 수 있다.
  • props check 후, prop에 변화가 있다면 새로 렌더링하고 변화가 없다면 기존에 렌더링된 컴포넌트를 재사용한다.
  • 고차 컴포넌트이다.
    (컴포넌트를 인자로 받아서 최적화된 컴포넌트를 반환해줌)

주의사항

React.memo 또한 메모리에 저장해두며 재사용하는 것이므로, 무분별하게 사용할 시 메모리 소비로 인해 성능에 독이 될 수 있다.

사용해야 되는 때

  1. 컴포넌트가 같은 props로 자주 렌더링 될 때
  2. 컴포넌트가 렌더링 될때마다 복잡한 로직을 처리해야 할 때

사용방법

const child = ({ name, age }) => {
  // (...)
};
export default memo(child);

최적화 시키고 싶은 컴포넌트를 감싸주면 된다.


9. Custom Hooks

개념

중복된 컴포넌트가 많은 경우 코드가 지저분해질 수 있다. 여러 컴포넌트 들의 반복되는 로직이 필요하다면 직접 훅을 만들어서 사용할 수 있다. 함수를 하나 만드는 것이다.

생성 및 사용방법

새로운 파일을 생성한다. 이때 파일명은 다른 훅처럼 앞에 use 키워드를 사용하는 것이 좋다. use 키워드 사용 시, 훅을 사용하면서 실수를 저지를 경우 리액트 자체에서 콘솔에 적절한 경고 메시지를 띄워준다.

export function useTest() {
  	(...)
	return (...)
}

다른 훅과 사용방법은 동일하다.




리액트를 다루는 기술 이라는 책을 통해 1차적으로 보고, 부족한 부분은 유튜브(별코딩-React Hooks에 취한다)를 통해 정리하였다. 기록하기 위해 여러번 보니, 확실히 처음 봤을 때보다는 이해가 많이 되었다. 이제 프로젝트를 통해 직접 사용해 보면서 내 것으로 만들어야겠다 !!

profile
🎨

0개의 댓글