React - Hooks

dobby·2024년 11월 28일
0
post-thumbnail

React Hook이 나오게 된 배경

📌 1. 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다.

  • Hook을 사용하면 컴포넌트로부터 상태 관련 로직을 추상화할 수 있다.
  • 독립적인 테스트와 재사용이 가능하다.
  • Hook은 계층의 변화 없이 상태 관련 로직을 재사용할 수 있게 도와준다.

📌 2. 복잡한 코드를 간단하게 나타낼 수 있다.

  • Hook을 통해 서로 비슷한 것을 하는 작은 함수의 묶음으로 컴포넌트를 나누는 방법을 사용할 수 있다.

📌 3. Class는 혼란을 줄 수 있다.

React에서의 Class 사용을 위해서는 JavaScript의 this 키워드가 어떻게 동작하는지 알아야만 한다.
Javascript의 this 키워드는 대부분의 다른 언어와는 다르게 작동함으로, 사용자에게 큰 혼란을 주었으며 코드의 재사용성과 구성을 매우 어렵게 만들었다.

사용자들은 props, state, 그리고 top-down 데이터 흐름을 완벽하게 하고도, class의 이해에는 어려움을 겪고는 했다.

이러한 문제를 해결하기 위해, Hook은 Class 없이 React 기능들을 사용하는 방법을 제시한다.
개념적으로 React 컴포넌트는 함수에 더 가까워 사용자가 이해하기 쉽다.

  • class 사용을 위해 필수적인 this 키워드가 어떻게 작동하는지 알아야 하기 때문에 이는 사용자들에게 큰 혼란을 주었으며, 코드의 재사용성과 구성을 어렵게 만들었다.
  • Hook은 Class 없이 React 기능을 사용하는 방법을 제시한다.

📌 4. Life Cycle로 인한 중복 로직을 피하기 위해서

생명주기 메소드에는 자주 관련 없는 로직이 섞여들어가고는 한다.
예시로 componentDidMountcomponenetDidUpdate는 컴포넌트 안에서 데이터를 가져오는 작업을 수행할 때 사용 되어야 하지만, 같은 componentDidMount에서 이벤트 리스너를 설정하는 것과 같은 관계없는 로직이 포함되기도 하며, componentWillUnmount에서 cleanup 로직을 수행하기도 한다.

Hook을 통해 서로 비슷한 것을 하는 작은 함수의 묶음으로 컴포넌트를 나누는 방법을 사용할 수 있다.
또한, 리듀서를 활용해 컴포넌트의 지역 상태 값을 관리하도록 할 수 있다.

아래 예시는 라이프 사이클에 중복된 로직을 사용하는 경우이다.

class Example extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			count: 0;	
		};
	}
	componentDidMount() {
		document.title = `You clicked ${this.state.count} times`;
	}
	componentDidUpdate() {
		document.title = `You clicked ${this.state.count} times`;
	}

	render() {
		return (
			<div>
				<p>You clicked {this.state.count} times</p>
				<button onClick={() => this.setState({count: this.state.count + 1})}>
				Click me
			</div>
		);
	}
}

위의 코드를 react hook의 useEffect를 사용하여 라이프사이클을 통합한 예제이다.

import {useState, useEffect} from 'react';
const Example = () => {
	const [count, setCount] = useState(0);
	useEffect(() => {
		document.title = `You clicked ${count} times`;
	});
	return (
		<div>
			<p>You clicked {count} times</p>
			<button onClick={() => setCount(count + 1)}>
				Click me
			</button>
		</div>
	);
}

hook인 useEffect를 사용하니 lifeCycle의 중복 로직 문제가 해결된 것을 볼 수 있다.


Hook

hook은 state 그 자체가 아니라, 상태 관련 로직을 재사용하는 방법이다.
실제로 각각의 hook 호출은 완전히 독립된 state를 가진다.
그래서 심지어 한 컴포넌트 안에서 같은 custom hook을 두 번 쓸 수도 있다.

Custom Hook은 기능이라기보단 컨벤션에 가깝다.
이름이 'use'로 시작하고, 안에서 다른 hook을 호출한다면 그 함수를 custom hook이라고 부를 수 있다.


📌 useEffect

컴포넌트 내의 상태 변화가 있을 때 이를 감지하여 특정 작업을 해준다.
일반적으로 sideEffect 처리를 위해 사용된다.

useEffect(()=>{
   const id = setTimeout(() => {
     ...
   }, 1000);
  
  return (
   clearTimeout(id); 
  )
}, []) // 의존성 배열

첫 번째 인자로 콜백 함수, 두 번째 인자로 의존성 배열을 받는다.
의존성 배열 안에 있는 값이 업데이트 될 때에만 콜백 함수를 다시 호출하여 effect를 수행하게 된다.
componentDidMount, componentDidUpdate를 합쳐놓은 것과 같다.

만약 빈 배열을 넣는다면, 마운트 될 때에만 값을 콜백 함수를 실행한다.
빈 배열이 아닌 아무것도 넣지 않는다면, 모든 업데이트에 대해 effect를 수행하게 된다.
불필요한 렌더링을 줄이고 싶다면, useEffect의 두 번째 인자인 의존성 배열을 넣어주어야 한다.

useEffect 안의 첫 번째 인자인 콜백함수 내 return 문은 해당 컴포넌트가 제거될 때(unmount) 실행된다.

useEffect 실행 타이밍

useEffect로 전달된 함수는 컴포넌트 렌더링 - 화면 업데이트 - useEffect 실행 순으로 실행된다.
즉, useEffect실행이 최초 렌더링 이후에 된다.

만약 화면을 다 그리기 이전에 동기화 되어야 하는 경우에는, useLayoutEffect()를 활용하여 컴포넌트 렌더링 - useLayoutEffect 실행 - 화면 업데이트 순으로 effect를 실행시킬 수 있다.


📌 useState

컴포넌트의 상태를 관리할 수 있게 해준다.

const [time, setTime] = useState(60);

useState에서 반환된 배열의 두 번째 값인 setter 함수를 호출하면 상태 값을 변경할 수 있고, 상태 값이 변경되면 해당 컴포넌트는 리렌더링된다.


📌 useRef

2 가지 경우에 useRef를 사용할 수 있다.

  1. DOM에 직접 접근할 때
    리액트는 DOM으로의 직접 접근을 막고 있기 때문에, ref를 통해 접근해야한다.
  2. 변수의 값이 변하더라도 리렌더링을 유발하고 싶지 않을 때
    ref의 값은 컴포넌트의 전 생애주기를 통해 유지되기 때문에, 컴포넌트가 브라우저에 마운팅 된 시점부터 마운트가 해제될 때까지 같은 값을 계속해서 유지할 수 있다.
const inputRef = useRef();

<input ref = {inputRef}/>

inputRef.currentinput 태그에 접근할 수 있게 된다.

ref를 하위 컴포넌트로 전달하고 싶다면, 하위 컴포넌트에선 forwardRef를 사용해야 한다.

function App() {
    const inputRef = useRef();
    
    return (
    	<div>
    		<Input ref={inputRef}/> // ref 전달 
			<button onClick={()=>inputRef.current.focus()}> Focus </button>
        </div>
	);
}
const Input = React.forwardRef(( _, ref ) => {
    return (
    <>
    	Input: <input ref={ref} />
    </>
    );
});

export default Input;

📌 useContext

react에서 데이터는 props를 통해 부모에서 자식에게 전달되지만, 어플리케이션 안의 여러 컴포넌트에게 props를 전달해줘야 하는 경우 context를 이용하면 명시적으로 props를 넘겨주지 않아도 값을 공유할 수 있다.

컴포넌트를 중첩하지 않고도 React Context를 구독할 수 있게 해준다.

function Example() {
	const locale = useContext(LocaleContext);
	const theme = useContext(ThemeContext);
}

context API를 사용하기 위해선 Provider, Consumer, createContext 세 가지 개념을 알아야 한다.

  • createContext: context 객체를 생성한다.
  • Provider: 생성한 context를 하위 컴포넌트에게 전달하는 역할을 한다.
  • Consumer: context의 변화를 감시하는 컴포넌트이다.
import { createContext } from 'react';

export const ThemeContext = createContext(null);
import {useState} from 'react';
import { ThemeContext } from "./context/ThemeContext";
import Page from './Components/Page';

function App() {
 const [isDark, setIsDark] = useState(false);
  
  return (
  	<ThemeContext.Provider value={{isDark, setIsDark}}>
    	<Page />
    </ThemContext.Provider>
  );
}

export default App;
import {useContext} from 'react';
import {ThemeContext} from '../context/ThemeContext';

const Header = () => {
 const {isDark, setIsDark} = useContext(ThemeContext);
 const toggleTheme = () => {
  setIsDark(!isDark); 
 }
 
  return (
    ...
  );
}

export default Header;

📌 useReducer

상태를 업데이트할 때 useState를 사용해서 새로운 상태를 설정해준다.
이 상태를 관리하게 될 때 useState를 사용하는 것 말고도 useReducer를 사용할 수 있다.

가장 큰 차이는 useReducer를 사용하면 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있다는 것이다.
상태 업데이트 로직을 컴포넌트 바깥에 작성할 수도 있고, 다른 파일에 작성 후 불러와서 사용할 수도 있다.

import React, {useReducer} from 'react';

const reducer = (state, action) => {
	switch(action.type) {
		case 'INCREMENT':
			return state + 1;
		case 'DECREMENT':
			return state - 1;
		default:
			return state;
	};
};

const Counter = () => {
	const [number, dispatch] = useReducer(reducer, 0);

	const onIncrease = () => {
		dispatch({type: 'INCREMENT'});
	};

	const onDecrese = () => {
		dispatch({type: 'DECREMENT'});
	};

	return (
		<div>
			<h1>{number}</h1>
			<button onClick={onIncrease}>+1</button>
			<button onClick={onDecrease}>-1</button>	
		</div>
	);
}

export default Counter;

useReducer의 사용법은 아래와 같다.

const [state, dispatch] = useReducer(reducer, initialState);
  • state: 컴포넌트에서 사용할 state
  • dispatch: reducer 함수를 실행시키며, 컴포넌트 내에서 state의 업데이트를 일으키기 위해 사용하는 함수
  • reducer: 컴포넌트 외부에서 state를 업데이트하는 로직을 담당하는 함수로, 현재의 state와 action 객체를 인자로 받아 기존의 state를 대체할 새로운 state를 반환하는 함수
  • initialState: 초기 state
  • action: 업데이트를 위한 정보를 가지고 있으며, dispatch의 인자

📌 useCallback

useCallback은 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용한다.
최적화의 한 방법으로 사용한다.

//수정 전
  import {useCallback} from 'react';
  const increament1 = () => {
    setCount1({ num: count1.num + 1 });
  };
  //수정 후
  const increament1 = useCallback(() => {
    setCount1({ num: count1.num + 1 });
  },[count1]);

useCallback의 첫 번째 인자로는 인라인 콜백과 의존성 값의 배열을 받게 된다.
useCallback(fn, deps)
의존성 배열인 deps에 변경을 감지해야할 값을 넣어주게 되면, count1이 변경될 때마다 콜백함수를 새로 생성하게 된다.


📌 useMemo

컴포넌트의 성능을 최적화하는데 사용되는 훅

useMemo는 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다.

const value = useMemo(() => {
    return calculate();
},[item])

useMemouseEffect처럼 첫 번째 인자로 콜백 함수, 두 번째 인자로 의존성 배열을 받는다.
의존성 배열 안에 있는 값이 업데이트 될 때에만 콜백 함수를 다시 호출하여 메모리에 저장된 값을 업데이트 해준다.

만약 빈 배열을 넣는다면, useEffect와 마찬가지로 마운트 될 때에만 값을 계산하고 그 이후론 계속 memoization된 값을 꺼내와 사용한다.


📌 React.memo

useMemo와 비슷하지만, 약간 다르다.
컴포넌트가 리렌더링 되는 조건은 다음과 같다.

  1. 컴포넌트의 상태가 변경될 때
  2. 부모로부터 받은 props가 변경될 때
  3. 부모 컴포넌트의 상태가 변경될 때

부모 컴포넌트의 상태가 변경되기만 해도 자식 컴포넌트가 리렌더링 될 수 있다.
부모 컴포넌트의 상태가 변경되었지만 자식 컴포넌트는 변경된 것이 없음에도 리렌더링 된다면, 비효율적이다.

이때 React.memo를 사용하여 부모 컴포넌트의 상태가 변경되더라도 자식 컴포넌트가 변경되지 않았을 경우 자식 컴포넌트의 리렌더링을 막을 수 있다.

const Box = React.memo(()=>{
    console.log("Box 컴포넌트 렌더링");
    return <div/>
})

export default Box;
function App(){
    const [count, setCount] = useState(0);
    
    return (
    	<div>
        	{count}
            <button onClick={()=>setCount(count+1)}>+</button>
        	<Box/>
        </div>
    )
}

export default App;

하지만, React.memo를 무분별하게 사용하면 오히려 성능 저하의 원인이 될 수도 있다.
변경 사항을 확인하기 위한 계산에도 비용이 필요하기에, 무분별하게 사용하게 되면 성능 저하가 올 수 있다.
그러니, 필요한 경우에만 사용하는 것이 좋다.


📌 사용자 정의 Hook

사용자 정의 Hook의 이름은 'use'로 시작되어야 한다.
이 관습을 따르지 않으면, 특정한 함수가 그 안에서 Hook을 호출하는지 알 수 없기 때문에 Hook 규칙의 위반 여부를 자동으로 체크할 수 없다.

사용자 정의 Hook은 상태 관련 로직을 재사용하는 매커니즘이지만, 사용자 Hook을 사용할 때마다 그 안의 state와 effect는 완전히 독립적이다.

각각의 Hook에 대한 호출은 서로 독립된 state를 받는데, React의 관점에서 이 컴포넌트는 useStateuseEffect 등의 hook을 호출한 것과 다름없다.


Hook의 규칙

hook은 자바스크립트 함수이다.
하지만, hook을 사용할 때는 두 가지 규칙을 준수해야 한다.

이 규칙들을 강제하기 위해 linter 플러그인을 제공하고 있다.
1. 최상위(at the Top Level)에서만 Hook을 호출해야 한다.
2. 오직 React 함수 내에서만 Hook을 호출해야 한다.


ESLint 플러그인

위의 두 가지 규칙을 강제하는 eslint-plugin-react-hooks라는 ESLint 플러그인을 React는 제공한다.

이 플러그인은 Create React App에 기본적으로 포함되어 있다.

// ESLint 설정 파일
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}

React가 어떤 특정 state가 호출되는지 알 수 있는 이유는 Hook이 호출되는 순서에 의존하기 때문이다.
조건문, 반복문 등의 내부에 Hook을 넣으면 안된다.
위의 호출 순서에 영향이 가게 되고, 순서가 밀리기 때문에 버그가 발생하게 된다.

조건부로 effect를 실행하기를 원한다면, 조건문을 Hook 내부에 넣을 수 있다.

useEffect(function persistForm() {
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

제공되는 Lint 규칙을 활용한다면 알아서 오류를 표시해주기 때문에 걱정할 필요는 없다.



참고
velog minw0_o

profile
성장통을 겪고 있습니다.

0개의 댓글