리액트 16.8 이전 버전에서는 function 컴포넌트에서는 상태(state)를 관리할 수가 없었다. 16.8 버전에서 Hooks 라는 기능이 도입되면서 function 컴포넌트에서도 상태를 관리할 수 있게 되었다. function 컴포넌트의 Hooks를 사용하면 class 컴포넌트에서만 사용할 수 있었던 라이프사이클을 사용할 수 있으며 React의 여러 기능을 함수형 프로그래밍으로 사용할 수 있게 해준다.
useState | 컴포넌트의 상태(state)를 관리 할 수 있다. |
---|---|
useEffect | 의존성 배열에 적힌 값이 변경될 때마다, 특정기능이 작동하도록 할 수 있다. |
useRef | 특정 DOM을 선택하거나 변수를 관리할 수 있다. |
useMemo | 의존성 배열에 적힌 값이 변할 때만 값을 다시 정의할 수 있다. |
useCallback | 의존성 배열에 적힌 값이 변할 때만 함수를 다시 정의할 수 있다. |
useContext | 여러개의 컴포넌트에서 사용할 수 있는 값을(변수, 함수 등) 만들 수 있다 |
useReducer | 상태(state) 업데이트 로직을 reducer 함수에 따로 분리 할 수 있다. |
useImperativeHandle (+ forwardedRef) |
useRef로 만든 래퍼런스를 상위 컴포넌트로 전달할 수 있다. (useImperativeHandle와 forwardedRef를 활용하면 부모 컴포넌트가 자식 컴포넌트의 함수를 호출하거나 값을 가져올 수 있다.) |
useLayoutEffect | useEffect와 비슷하지만, 모든 DOM 변경 후 브라우저가 화면을 그리기(render)전에 실행되는 기능을 정할 수 있다는 차이점이 있다. |
useDebugValue | 리액트 개발자도구에서 사용자 Hook 레이블을 표시하는 데에 사용할 수 있다. (사용자 정의 Hook의 디버깅을 도와준다.) |
리액트는 state나 props의 값이 변경되면 이를 감지하고 리렌더링을 한다. state는 화면을 바꿔주기위해 사용되는 트리거역할을 하는 값이며 useState
는 state의 값을 변경해주는 Hook이다.
const [state, setState] = useState(기본값);
// 수량 변경 예시
import React, { useState } from 'react';
const Sample = () => {
const [number, setNumber] = useState(0);
const increase = () => setNumber((prev) => prev + 1);
const decrease = () => setNumber((prev) => prev - 1);
return (
<div>
<h1>{number}</h1>
<button type="button" onClick={increase}>
+1
</button>
<button type="button" onClick={decrease}>
-1
</button>
</div>
);
};
export default Sample;
useEffect
는 컴포넌트가 마운트 됐을 때 (처음 나타났을 때), 언마운트 됐을 때 (사라질 때), 그리고 업데이트 될 때 (특정 props가 바뀔 때) 특정 작업을 처리할 수 있다.
useEffect
는 class 컴포넌트의 라이프 사이클 메서드인 componentDidMount(), componentWillUnmount(), componentDidUpdate()가 합쳐진 것으로 생각하면 된다.
clean-up이란?
파라미터로 넣은 함수의 return 함수이다. 컴포넌트가 Unmount 될 때만 cleanup 함수를 실행시키고 싶다면 deps에 빈 배열을, 특정 값이 업데이트되기 직전에 cleanup 함수를 실행시키고 싶다면 deps에 해당 값을 넣어주면 된다. (class 라이프 사이클의 componentWillUnmount와 비슷)
import React, { useState, useEffect } from 'react';
const Sample = (props) => {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
const handleStatusChange = (status) => setIsOnline(status.isOnline);
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// effect 이후에 어떻게 정리(clean-up)할 것인지 표시
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, []);
if (isOnline === null) return 'Loading...';
return isOnline ? 'Online' : 'Offline';
};
export default Sample;
일반적으로 JS에서 DOM을 선택해야 하는 상황에선 getElementById
, querySelector
와 같은 DOM selector 함수를 사용해서 특정 DOM을 선택한다. 리액트에서도 DOM을 선택해야 하는 상황이 발생할 수 있으며 useRef
를 사용한다.
Ref 객체의 .current 값으로 원하는 DOM을 선택한다.
import React, { useRef } from 'react';
const Sample = () => {
const name = useRef();
const onClick = () => {
name.current.focus();
};
return (
<div>
<input name="name" placeholder="이름을 입력해주세요." ref={name} />
<button onClick={onClick}>input값에 foucs!</button>
</div>
);
};
export default Sample;
useRef는 일반적으로 컴포넌트에서 특정 DOM을 선택해야 할때 사용하지만 컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리할 때도 사용할 수 있다. useRef로 관리하는 변수는 값이 바뀐다고 해서 컴포넌트가 리렌더링 되지 않는다.
/* https://microcephalus7.github.io/react/js/377 */
// 1. useState를 사용했을 때
const [number, setNumber] = useState(0);
let timer = setInterval(() => {
setNumber(number + 1);
console.log(number); // 0 0 0 0 0 ...
}, 1000);
// 2. useRef를 사용했을 때
const number = useRef(0);
let timer = setInterval(() => {
number.current = number.current + 1;
console.log(number); // 0 1 2 3 4 ...
}, 1000);
리액트에서 useState
를 사용했을 때 prevState와 nextState를 비교하여 값을 변경한다.
setInterval의 내부 callback의 closure는 prevState를 참조하며 참조로 인하여 prevState는 메모리에 남게되고 가비지 컬렉터의 영향을 받지 않는다.
setStae로 state를 변경하더라도 closure에 의해 state 값은 변경되지 않고 console에는 같은 값이 찍힌다. (0 0 0 ...)
리액트에서 useRef
를 사용하면 순수한 JS 객체를 생성한다.
useState
처럼 값을 비교하는 과정이 없으며 .current로 값을 변경하면 JS 객체도 그대로 변경된다.
setInterval의 내부 callback의 closure에는 변경되는 .current를 참조한다.
.current로 값을 변경 시 값이 변경되면서 console에는 다른 값이 찍히게 된다. (1 2 3 ...)
setInterval, setTimeout 및 리렌더링에 영향을 주지않는 값들은 useRef를 참조하여 값을 변경한다.
useMemo
는 메모이제이션된 값을 return하는 Hook으로 성능최적화에 쓰인다. useMemo
는 두번째 인자(deps)로 준 인자 중에 하나라도 변경되면 값을 재계산하며 컴포넌트가 리렌더링 될 때마다 소요되는 불필요한 계산을 피할 수 있다.
만약 deps에 아무것도 전달하지 않는다면, 렌더시
useMemo
로 전달된 함수는 렌더링 중에 실행되므로, 렌더링 중에 하지 않는 것을 이 함수 내에서 처리하면 안되고 useEffect
내에서 처리해야 한다.
useMemo(() => {}, []);
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
React.memo
?
const MyComponent = (props) => {
/* props를 사용하여 렌더링 */
};
cosnt function areEqual = (prevProps, nextProps) =>{
/*
nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
*/
}
export default React.memo(MyComponent, areEqual);
useMemo와 React.memo의 공통점
React.memo
와 useMemo
모두 props가 변하지 않으면 인자로 넘긴 함수는 재실행되지 않고, 이전의 메모이즈된 결과를 사용한다.
useMemo와 React.memo의 차이점
useCallback
은 useMemo와 비슷한 Hook으로 성능최적화에 사용되는 메모제이션 Hook이다.
useMemo
는 특정 결과값을 재사용 할 때 사용하는 반면, useCallback
은 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용한다.
useCallback(() => {}, []);
// 함수 안에서 사용하는 상태 혹은 props 가 있다면 꼭, deps 배열안에 포함시켜야 한다.
// props 로 받아온 함수가 있다면, 이 또한 deps에 꼭 포함시켜야 함.
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
리액트는 state를 상위 컴포넌트에서 관리하고 해당 state를 하위 컴포넌트 props로 내려주는데 하위 컴포넌트가 많아질 수록 props를 통해 하위로 계속 전달해야 하기 때문에 컴포넌트가 복잡해 지고 코딩이 불편해진다.
리액트의 기본 기능인 context API를 사용하면 모든 컴포넌트가 context에 있는 state를 자유롭게 사용할 수 있어 편리해진다. (Redux가 contextAPI 기반으로 만들어졌다고 한다.)
useContext
는 props를 아무 컴포넌트에서도 조회할 수 있게 도와주는 Hook이다. context 객체를 받아 해당 context의 현재 값을 반환한다.
Provider로 감싼 하위 컴포넌트에 props를 전달하지 않아도 어디서든 state에 접근을 할 수 있게 된다.
const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
const ThemeContext = React.createContext(themes.light);
// Context.Provider와 useContext를 같이 사용
const App = () => {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
};
const Toolbar = (props) => {
return (
<div>
<ThemedButton />
</div>
);
};
const ThemedButton = () => {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
};
useReducer
는 useContext
로 내려받은 state에 대해서 state관리를 할 수 있게 된다.
useReducer
를 사용하면 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있으며 상태 업데이트 로직을 컴포넌트 바깥에 작성 할 수도 있고, 다른 파일에 작성 후 불러와서 사용 할 수도 있다.
useState
의 대체 함수이며 (state, action) => newState의 형태로 reducer를 받고 dispatch 메서드와 짝의 형태로 현재 state를 반환한다.
const [state, dispatch] = useReducer(reducer, 초기값);
reducer란? reducer는 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수다.
const initialState = { count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
};
리액트의 함수형 컴포넌트에서 로직을 쉽게 재사용하기 위해 Custom Hook을 만들 수 있다. 주로 재사용 되는 로직들을 커스텀 훅으로 만들어 사용한다고 한다.
자주사용하는 Custom Hook (1~14번까지는 npm으로 설치 가능)
이 외에도 https://nikgraf.github.io/react-hooks/에서 다양한 custom hook을 확인할 수 있다.
-참고