일반적으로 많이 사용하는 react hook에 대해 알아보겠습니다.
Hook
Hook은 React v.16.8부터 새로 추가되었습니다.
함수형 컴포넌트에서도 life cycle을 효과적으로 관리하며 기존의 방식보다 직관적인 API를 제공합니다.
또한, 상태 관리 로직을 추상화 할 수 있는 장점도 있습니다.
render porps이나 HOC와 같은 패턴으로 재사용 로직을 구현했을 당시에는 컴포넌트의 재구성을 강제하고 코드의 추적이 어려웠습니다. (wrapper hell)
Hook을 사용한다면 컴포넌트마다 로직을 추상화하여 독립적인 테스트와 재사용이 가능합니다. 또한 계층의 변화없이 로직을 재사용할 수 있습니다.
그렇기 때문에 로직 기반으로 컴포넌트를 나눌 수 있어 함수 단위로 관리 할 수 있는 이점이 있습니다.
(기존 클래스형 컴포넌트는 라이프 사이클 메서드 기반으로 상호 관계없는 로직들이 공존했으며 분리도 불가능했습니다.)
종합적으로 살펴봤을 때, 클래스형 컴포넌트의 많은 불편함과 비효율성 (러닝커브를 높이는 this 키워드, 컴포넌트의 구조 표준의 부재? 등의 문제) 때문에 Class없이 React를 사용하기 위해 탄생한 친구라고 생각됩니다.
Hook의 규칙
Hook을 사용할 때 준수해야 하는 두 가지 규칙입니다.
이 두 가지 규칙은 eslint-plugin-react-hooks
라는 ESlint 플러그인을 통해 강제됩니다.
플러그인은 프로젝트에 추가할 수 있고, cra에 기본적으로 포함되어 있습니다.
1. 최상위에서만 Hook을 호출해야 한다.
컴포넌트가 렌더링 될때마다 동일한 순서로 Hook이 호출되는 것을 보장하기 위해 반복문, 조건문 혹은 중첩된 함수 내에서 호출하면 안됩니다.
그래야 useState
와 useEffect
의 호출이 반복되더라도 Hook의 상태를 올바르게 유지 할 수 있습니다.그렇기 때문에 Hook은 항상 React의 최상위에서 호출해야합니다.
2. React 함수 내에서만 Hook을 호출해야 한다.
React 함수 컴포넌트 또는 Custom Hook에서 호출해야 합니다. 일반적인 JS함수에서는 호출하면 안됩니다.
useState
첫 번째로 알아볼 Hook은 상태를 관리하는 State Hook인 useState
입니다.
useState
는 초기값을 설정할 수 있습니다. 이는 첫 렌더링에서 사용되며,
반환값으로는 상태를 나타내는 state와 상태를 변경하는 setState를 한 쌍으로 제공합니다.
클래스와 달리 객체일 필요는 없고, 문자, 숫자, boolean, 배열, null, 객체 등 여러가지 값을 설정할 수 있습니다.
import React, { useState } from 'react';
function Example() {
// 새로운 state 변수를 선언하고, count라 부르겠습니다.
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
초기값 0을 갖는 state인 count를 설정하고 버튼을 클릭하면 setState를 통해 count를 변경하는 간단한 예시입니다.
최적화를 위해 위의 예시보다는 다음과 같은 방식이 더 적절합니다.
setState(prev => prev+1);
useState는 이전 state값을 인자로 받아와는 콜백함수를 인자로 받을 수 있습니다.
useEffect
다음은 렌더링될 때마다 호출되는 Hook인 useEffect
입니다.
보통 side effect를 수행하는데 사용됩니다.
class 생명주기 메서드 중 세 가지
componentDidMount
, componentDidupdate
, componentWillUnmount
가 합쳐진 것으로 생각해도 됩니다.
기본적인 형태는 useEffect(function, deps)이며,
function의 반환과 deps의 여부에 따라 동작이 달라집니다.
먼저, deps가 없는 경우,
componentDidMount
, componentDidupdate
방식을 구현할 수 있습니다.
// mount와 update 모두 해당
useEffect(() => {
document.addEventListener('mousedown', handleOutsideClick);
});
렌더링 이후에 항상 같은 코드가 반복되는 것을 원한다면 기존의 생명 주기 메서드에서는 두 가지에 중복된 코드를 담아야 했지만, useEffect
는 mount와 update모두에서 동작하므로 코드가 중복되지 않습니다.
만약 deps가 빈 배열로 있는 경우
componentDidMount
만 구현되며, 다음과 같이 작성할 수 있습니다.
// 의존성 배열에 빈 배열 할당
useEffect(() => {
console.log('mount에서만 동작합니다.')
}, []);
마지막으로 deps에 값을 넣어주는 경우
componentDidMount
에 더해서 선택적으로 componentDidupdate
를 동작할 수 있습니다.
// 의존성 배열에 값 할당
useEffect(() => {
console.log('mount됐을 때와 data가 변경되었을 때 동작합니다.')
}, [data]);
다음과 같이 작성한다면 의존성 배열에 담긴 값이 변경될 때마다 호출됩니다.
또한, 함수의 반환을 통해 componentWillUnmount
와 같이 구현할 수 있습니다.
그렇기 때문에 clean-up을 실행하기 위해 또 다른 effect를 구현할 필요가 없습니다.
effect가 함수를 반환하면, React는 그 함수를 clean-up할 때 이를 실행합니다.
import React, { useEffect } from 'react';
function Example() {
useEffect(() => {
document.addEventListener('mousedown', handleOutsideClick);
return () => document.removeEventListener('mousedown', handleOutsideClick);
});
return (
...
);
}
다음과 같이 등록한 이벤트를 unmount시 제거할 수 있습니다.
각기 다른 side effect를 관리하기 위해 여러개의 useEffect를 사용할 수 있지만,
호출은 항상 위에서부터 실행되는 것에 유의해야 합니다.
useRef
useRef는 ref를 사용해야 할 때 사용하는 Hook입니다.
JS에서는 특정 DOM을 선택하기 위해 getElementById
, querySelector
같은 DOM Selector 함수를 사용합니다.
React에서도 이런 상황에서 DOM을 선택하기 위해 ref를 사용하며, 함수형 컴포넌트에서는 useRef를 활용합니다.
function Sample() {
const btnRef = useRef();
const onClick=()=>{
btnRef.style.color="blue";
};
return (
<div>Ref 예제</div>
<button
onClick={onClick}
ref={btnRef}
/>
)
}
이 외에도 ref를 활용하여 조회 및 수정할 수 있는 변수를 관리할 수도 있습니다.
useRef는 useState와 달리 관리하는 값이 변해도 리렌더링을 발생시키지 않습니다.
따라서 외부 라이브러리를 사용한 인스턴스나 setTimeout의 id값 같이 화면에 그려지는 값들에 영향을 끼지치 않고
관리하는 로컬 변수들을 주로 관리합니다.
+ mount 여부를 판단하여 componentDidUpdate를 구현할 수도 있습니다.
useCallback
useCallback은 함수를 메모이제이션(memoization) 하기 위한 Hook입니다.
보통 리액트의 렌더링 성능을 개선하기 위해 사용합니다.
useCallback(fn, deps)
메모이제이션 할 함수와 의존성 배열이 필요합니다.
의존성이 변경될 때마다 메모이제이션 함수가 변경됩니다.
이를 통해 불필요한 함수 생성을 미연에 방지할 수 있습니다.
function App() {
const [name, setName] = useState('');
const onSave = () => {};
return (
<div className="App">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Profile onSave={onSave} />
</div>
);
}
이런 컴포넌트가 있다고 가정했을 때, name이 변경되면 랜더링이 일어납니다.
그에 따라 onSave은 같은 역할을 수행하지만, 함수를 비교하고 재할당하는 과정이 들어가게 됩니다.
function App() {
const [name, setName] = useState('');
const onSave = useCallback(() => {
console.log(name);
}, []);
return (
<div className="App">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Profile onSave={onSave} />
</div>
);
}
다음과 같이 useCallback으로 onSave 함수를 감싸면, 첫 렌더링 시에 메모이제이션 함수를 onSave에 할당합니다.
물론, 이 경우에는 name이 무슨 값이든 onSave를 실행시키면 빈 문자열만 출력됩니다.
그렇기 때문에 함수에 영향을 미치지 않는 다른 State때문에 렌더링이 일어났을 때, 함수의 재생성을 막기 위해 사용하는 것이 일반적입니다.
useMemo
useCallback과 마찬가지로 메모이제이션 된 값을 반환합니다.
반환된 값이 함수라면 useCallback과 같은 기능을 하게 됩니다.
useMemo(fn, deps)
이 예제가 useMemo 사용법을 이해하기 가장 좋은 것 같아서 가져왔습니다.
import { useState } from "react";
// 어려운 계산기
const hardCalculate = (number) => {
for (let i = 0; i < 99999999; i++)
return number + 10000;
};
// 쉬운 계산기
const easyCalculate = (number) => {
return number + 1;
};
function App() {
const [hardNumber, setHardNumber] = useState(1);
const [easyNumber, setEasyNumber] = useState(1);
const hardSum = hardCalculate(hardNumber);
const easySum = easyCalculate(easyNumber);
return (
<div>
<h1>어려운 계산기</h1>
<input
type="number"
value={hardNumber}
onChange={(e) => setHardNumber(parseInt(e.target.value))}
/>
<span> + 10000 = {hardSum}</span>
<h1>쉬운 계산기</h1>
<input
type="number"
value={easyNumber}
onChange={(e) => setEasyNumber(parseInt(e.target.value))}
/>
<span> + 1 = {easySum}</span>
</div>
);
}
export default App;
쉬운 계산기는 바로 계산해주는 반면, 어려운 계산기는 99,999,999번의 반복문을 돌린 뒤 계산을 해줍니다.
하지만 실제로 쉬운 계산기의 input을 변경했을 때나, 어려운 계산기의 input을 변경했을 때나 똑같이 약간의 딜레이를 갖게 됩니다.
그 이유는 input을 통해 number을 변경하게 되면, 그것이 hardNumber이든 easyNumber이든 관계없이 컴포넌트가 리렌더링되는데, 그 과정에서 hardsum이라는 변수도 재할당되게 됩니다.
그렇기 때문에 매 렌더링마다 hardsum을 계산하기 위해 호출되는 함수 hardCalculate()에 의해 딜레이가 생기는 것입니다.
이를 방지하기 위해 useMemo를 활용할 수 있습니다.
const hardSum = useMemo(() => {
return hardCalculate(hardNumber);
}, [hardNumber]);
다음과 같이 hardNumber가 변경되었을 경우에만 hardSum을 재할당해주면 이제 쉬운 계산기는 딜레이없이 사용할 수 있게 됩니다.
Reference
'개발일기 > Web' 카테고리의 다른 글
[React] 검색 요청 최적화하기 with Debounce (1) | 2023.08.25 |
---|---|
[React] 리액트에서 Drag&Drop으로 파일 업로드하기 (0) | 2023.06.27 |
[React] 리액트 클래스형 컴포넌트의 생명 주기 메서드(Life Cycle) (0) | 2023.06.20 |
[npm vs yarn] yarn은 무엇이 다를까? (0) | 2023.06.12 |
[Babel] 바벨? (0) | 2023.05.25 |