리액트의 훅은 16.8 버전부터 새로 추가된 기능이다. 기존에는 Class형 컴포넌트에서만 상태를 관리 할 수 있었고, 함수형 컴포넌트에서는 상태를 관리할 수 없었지만, Hook을 통해 상태 관리를 할 수 있게 되었고, 상태 관리 뿐만 아니라 기존 클래스형 컴포넌트에서만 가능하던 여러 기능을 사용할 수 있게 되었다.
useState는 리액트에서 기본으로 제공해주는 훅 함수들 중 하나로 컴포넌트에 상태 변수를 추가할 수 있도록 해주는 함수이다.
useState는 2개의 원소를 갖는 배열이 반환되며, 첫번째 원소는 상태값, 두번째 원소는 상태값을 변경할 때 사용되는 setter 함수가 반환된다. useState는 보통 아래와 같이 구조 분해 할당을 이용하여 선언하게 된다.
const [state, setState] = useState(initialState);
useState에서 반환된 배열의 두번째 값인 setter 함수를 호출하면 상태 값 변경과 함께 렌더링도 다시 진행된다.
useState의 setter 함수를 호출한 이후 바로 다음 줄에서 해당 state 값을 참조하면 아직 바뀌기 이전 state 가 참조된다. useState의 setter 함수가 비동기적으로 동작하기 때문이다.
컴포넌트가 렌더링 될 때 특정 작업을 실행할 수 있도록 하는 React Hook이다.
클래스형 컴포넌트에서 사용하던 라이프사이클 훅을 함수형 컴포넌트에서 useEffect로 대체할 수 있게 되었다. (componentDidMount, componentDidUpdate, componentWillUnmount)
하나의 컴포넌트에서는 useEffect를 여러개 작성하는 것이 가능하다.
useEffect(() => {
const listner = (event: UIEvent) => {
console.log(`[${performance.now()}] window size 변경됨!`);
};
window.addEventListener('resize', listner);
return () => {
window.removeEventListener('resize', listner);
};
}, []);
clean-up 코드는 useEffect Hook 내에서 return되는 함수를 말한다. 컴포넌트가 사라질 때(unmount 시점), 특정 값이 변경되기 직전(deps update 직전)에 실행할 작업을 지정할 수 있다.
useEffect 가 여러개 작성되어 있는 경우에는 제일 먼저 작성 되어 있는 useEffect 부터 제일 마지막에 작성되어 있는 useEffect 순으로 순서대로 호출된다.
React 공식 문서에 쓰여있는 설명에는, 'context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다' 라고 적혀있다.
전역적인 데이터를 전달하기에 편리하다.
App안에서 전역적으로 사용되는 데이터들을 여러 컴포넌트들끼리 쉽게 공유할 수 있는 방법을 제공해준다.
Props를 일일이 전해주는 것이 아니라 상위 컴포넌트에서 필요한 컴포넌트에게 전해줄 수 있게 된 것이다.
어플리케이션 안의 여러 컴포넌트들에게 props를 전달해줘야 하는 경우 context를 이용하면 명시적으로 props를 넘겨주지 않아도 값을 공유할 수 있게 해주는 것이다. 데이터가 필요할 때마다 props를 통해 전달할 필요가 없이 context 를 이용해 공유할 수 있다고 한다.
context API를 사용하기 위해서는 Provider , Consumer , createContext 이렇게 세가지 개념을 알고 있으면 된다.
이유 : Context를 사용하면 컴포넌트를 재사용하기 어려워질 수 있다.
import React, { createContext } from "react";
import Children from "./Children";
// AppContext 객체를 생성한다.
export const AppContext = createContext();
const App = () => {
const user = {
name: "김채원",
job: "가수"
};
return (
<>
<AppContext.Provider value={user}>
<div>
<Children />
</div>
</AppContext.Provider>
</>
);
};
export default App;
import React from "react";
import { AppContext } from "./App";
const Children = () => {
return (
<AppContext.Consumer>
{(user) => (
<>
<h3>AppContext에 존재하는 값의 name은 {user.name}입니다.</h3>
<h3>AppContext에 존재하는 값의 job은 {user.job}입니다.</h3>
</>
)}
</AppContext.Consumer>
);
};
export default Children;
하지만 Context는 코드가 늘어나면 여러 컴포넌트에서 사용이 가능하지만 코드가 점점 더러워지는 문제가 생길 것이다.
import React, { useContext } from "react";
import { AppContext } from "./App";
const Children = () => {
// useContext를 이용해서 따로 불러온다.
const user = useContext(AppContext);
return (
<>
<h3>AppContext에 존재하는 값의 name은 {user.name}입니다.</h3>
<h3>AppContext에 존재하는 값의 job은 {user.job}입니다.</h3>
</>
);
};
export default Children;
useContext 를 사용하면 기존의 Context 사용 방식보다 더 쉽고 간단하게 Context를 사용이 가능하고, 앞서 다뤘던 useState, useEffect와 조합해서 사용하기 쉽다.
useReducer 는 useState 보다 컴포넌트에서 더 다양한 상황에 따라 다양한 상태를 다른 값으로 업데이트해주고 싶을 때 사용하는 Hook이다.
useReducer를 사용하는 경우
useMemo를 사용하면 함수형 컴포넌트 내부에서 발생하는 연산을 최적화할 수 있다.
그래서 useMemo는 보통 복잡한 계산을 저장해놓는 용도나 참조 동일성 유지를 하는데 쓰인다.
여기서 참조 동일성 유지란, 객체나 배열이 리렌더링될 때마다 매번 주소가 달라지기 때문에 React에서는 렌더링될 때마다 객체나 배열이 달라졌다고 인식한다. 이럴 때 객체나 배열같은 곳에 useMemo를 써서 리렌더링을 막을 수 있다.
먼저 리스트에 숫자들을 추가하면 해당 숫자들의 평균을 나타내는 함수형 컴포넌트로 예시를 들어보겠다.
import React, { useState } 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 = e => {
setNumber(e.target.value);
};
const onInsert = e => {
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(list)}
</div>
</div>
);
};
export default Average;
이 코드에서는 숫자를 등록할 때 뿐만 아니라 인풋 내용이 수정될 때도 getAverage() 함수가 실행된다. 이렇게 렌더링할 때마다 계산을 하는 것은 낭비이다.
그래서 useMemo Hook을 사용하면 이 작업을 최적화할 수 있다.
import React, { useState, 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 = e => {
setNumber(e.target.value);
};
const onInsert = e => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber('');
};
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;
아래와 같이 useMemo를 쓰면 list 배열의 내용이 바뀔 때만 getAverage() 함수를 호출시키도록 할 수 있다.
const avg = useMemo(() => getAverage(list), [list]);
자식 컴포넌트에서 보통 쓰인다. 부모 컴포넌트로부터 받은 props의 값이 변할 때만 자식 컴포넌트가 렌더링되게 할 수 있다.
export const MemoizedMovie = React.memo(Movie);
useCallback은 useMemo와 비슷한 함수이다.
이 Hook을 사용하면 이벤트 핸들러 함수를 필요할 때만 생성할 수 있다.
import { useEffect, useState } from "react";
function App() {
const [number, setNumber] = useState(0);
const someFunction = () => {
console.log(`someFunc: number : ${number}`);
return;
};
// 📌 의존성 배열에 someFunction 넣어줬다..
useEffect(() => {
console.log("📌 someFunction 이 변경되었습니다.");
}, [someFunction]);
return (
<div>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<br />
<button onClick={someFunction}>Call someFunc</button>
</div>
);
}
export default App;
여기서 input의 number를 증감시켜줄 때마다 useEffect가 불린다. state를 변경할 때마다 컴포넌트는 리렌더링이 되기 때문이다. number가 바뀌어서 App 컴포넌트가 렌더링이 되면 someFuction 함수 안의 함수 객체가 다시 새로 만들어져서 또 다른 메모리 공간 안에 저장이 된다. 그럼 someFuction 변수 안에는 이전과는 다른 메모리 주소가 들어간다. 때문에 useEffect 입장에서는 이전 렌더링과 그다음 렌더링 때의 someFuction 안의 주소값을 비교했을 때 다르다고 인식을 하는 것이다.
다시 정리하자면 someFuction는 함수 객체가 들어있는 메모리의 주소를 가지고 있고 App 컴포넌트가 렌더링이 되어서 someFunction이 초기화되면 그 안에 있는 함수 객체가 새로 만들어져서 다른 메모리 공간에 저장되기 때문에 새로 만들어진 주소가 someFUnction 주소 안에 들어가게 된다. 그래서 useEffect는 someFunction 안에 있는 주소가 바뀌었으니 콜백 함수를 호출하는 것이다.
이때 useCallback을 사용하면 App 컴포넌트가 렌더링이 되어도 someFunction이 바뀌지 않게 할 수 있다.
import { useEffect, useState, useCallback } from "react";
function App() {
const [number, setNumber] = useState(0);
// 📌
const someFunction = useCallback(() => {
console.log(`someFunc: number : ${number}`);
return;
}, []);
useEffect(() => {
console.log("someFunction 이 변경되었습니다.");
}, [someFunction]);
return (
<div>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<br />
<button onClick={someFunction}>Call someFunc</button>
</div>
);
}
export default App;
하지만 의존성 배열에 아무것도 넣어주지 않아서 number 값이 변해도 해당 함수는 메모이제이션 해줬을 당시의 number 값이 0이어서 계속 콘솔에 0이라고 찍힐 것이다.
number 가 업데이트될 때마다 메모이제이션된 함수도 업데이트해주고 싶으면 두 번째 의존성 배열에 number를 넣어준다.
const someFunction = useCallback(() => {
console.log(`someFunc: number : ${number}`);
return;
}, [number]); // 📌 number 추가
이러면 number가 바뀔 때만 someFunction 함수가 초기화가 된다.
useRef는 페이지가 리렌더링 되어도 useState처럼 값이 남아있게 된다. 하지만 useState와 다른점은 useRef의 값이 변해도 렌더링이 일어나지는 않는다.
또, 자바스크립트에서의 getElementById와 비슷한 역할도 할 수 있다.
Average 컴포넌트에서 등록 버튼을 눌렀을 때 포커스가 인풋 쪽으로 넘어가도록 하는 코드
import React, { useState, useMemo, useRef } 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 inputEl = useRef(null);
const onChange = useCallback(e => {
setNumber(e.target.value);
}, []); // 컴포넌트가 처음 렌더링 될 때만 함수 생성
const onInsert = useCallback(
e => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber('');
inputEl.current.focus();
},
[number, list]
); // number 혹은 list 가 바뀌었을 때만 함수 생성
const avg = useMemo(() => getAverage(list), [list]);
return (
<div>
<input value={number} onChange={onChange} ref={inputEl} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균 값:</b> {avg}
</div>
</div>
);
};
export default Average;
useRef를 사용하여 ref를 설정하면, useRef를 통해 만든 객체 안의 current 값이 실제 엘리먼트를 가리키게 된다.
커스텀 훅이란? 반복되는 로직을 리액트 내장 훅 들을 사용하여 구현한 '자신이 만드는 훅'이라고 생각하면 된다.
쉽게 설명하자면 반복되는 로직을 분리했을 때
분리한 로직 속에 리액트 훅이 있다 : '커스텀 훅'
분리한 로직 속에 리액트 훅이 없다 : '(JS)함수'
"use"로 시작해야 한다.다음 코드는 useCounter라고 이름을 지은 커스텀 훅 예제 코드이다.
import { useState } from 'react';
// 커스텀 훅 정의
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
// count를 증가시키는 함수
function increment() {
setCount(count + 1);
}
// count를 감소시키는 함수
function decrement() {
setCount(count - 1);
}
// count를 리셋하는 함수
function reset() {
setCount(initialValue);
}
// 현재 count와 관련된 값을 반환하는 함수
function getCount() {
return count;
}
return {
count,
increment,
decrement,
reset,
getCount,
};
}
// 커스텀 훅 사용 예제
function Counter() {
const counter = useCounter(0);
return (
<div>
<p>Count: {counter.count}</p>
<button onClick={counter.increment}>증가</button>
<button onClick={counter.decrement}>감소</button>
<button onClick={counter.reset}>리셋</button>
</div>
);
}
export default Counter;
출처: https://ccomccomhan.tistory.com/273 [[꼼꼼한 개발자] 꼼코더:티스토리]
import { useState, useEffect } from 'react';
export default function usePromise(promiseCreator, deps) {
const [resolved, setResolved] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const process = async () => {
setLoading(true);
try {
const result = await promiseCreator();
setResolved(result);
} catch (e) {
setError(e);
}
setLoading(false);
};
useEffect(() => {
process();
}, deps);
return [loading, resolved, error];
}
그리고 이 usePromise 훅을 사용하는 컴포넌트
import React from 'react';
import usePromise from './usePromise';
const wait = () => {
// 3초 후에 끝나는 프로미스를 반환
return new Promise(resolve =>
setTimeout(() => resolve('Hello hooks!'), 3000)
);
};
const UsePromiseSample = () => {
const [loading, resolved, error] = usePromise(wait, []);
if (loading) return <div>로딩중..!</div>;
if (error) return <div>에러 발생!</div>;
if (!resolved) return null;
return <div>{resolved}</div>;
};
export default UsePromiseSample;
참고한 velog : https://velog.io/@velopert/react-hooks
왜 쓰다 마셨나요