[새싹 프론트엔드] 12/5~7 (Hooks)
Hooks란?
- 리액트 버전 16.8에 새로 도입된 기능
- 함수형 컴포넌트에서 상태 관리를 할 수 있는 기능 제공
- useState
- useRef
- useEffect
- useReducer
- useMemo
- useCallback
- useRef
1. useState()
가장 기본적인 Hook로 함수형 컴포넌트가 가변적인 상태를 지닐 수 있도록 해 준다.
const [state, setState] = useState(초기값);
state는 변수, 값이라고 생각하면 되고 setState는 state의 값을 바꿔주는 상태 변환 함수이다.
uploadInput() 함수 수정
state의 기존 값을 유지하면서 새로운 값을 추가하려면 setState()의 콜백 함수에 prevState 값을 전달해서 유지해야 한다.
setState((prevState) => [input, ...prevState]);
setState([input, ...state]);
setState() 함수 동작 과정
- 현재 state에서 setState가 실행된다면 state 값이 setState의 값으로 업데이트가 된다. 업데이트가 되는 순간 render() 함수가 자동 실행되며 이후 화면이 업데이트가 된다.
- setState() 함수의 인자로 state를 전달 시 동작 과정
이전 state와 새로운 state를 비교하여 바뀐 데이터만 업데이트한다. 변경되지 않은 값은 그대로 유지한다.
※ 화면이 바로 바뀌는 게 아니라 render() 함수가 실행되면서 바뀐 부분만 발견한 후 화면을 업데이트하는 것
useState() 성능 최적화
useState() 함수의 인자에 초기값을 무겁게 지정한 경우
- state 값이 업데이트 될 때마다 초기값이 계속해서 호출된다.
- 만약 초기값에 복잡한 계산식이 있다면 성능 저하 문제가 발생한다.
const [names, setNames] = useState(heavyWork());
function heavyWork() {
for(leti=0, i<1000, i++){
console.log(`엄청 복잡한 계산 중.. 시간 오래 걸림...`)
}
return ["정수아", "리액트"];
}
setNames는 prevState를 받아서 값을 갱신하므로 state가 업데이트될 때마다 heavyWork()가 계속 호출된다. 성능 최적화를 위해서는 초기값은 최초 한 번만 호출되도록 수정해야 하며 useState() 함수의 인자로 콜백 함수를 넣어주는 방법으로 해결이 가능하다.
const [names, setNames] = useState(() => heavyWork());
2. useRef()
컴포넌트 내부에서 사용되는 변수를 저장하는 Hook
- 컴포넌트가 재렌더링되어도 저장된 변수 값을 유지
- 불필요한 렌더링을 방지할 수 있음
- 특정 DOM 요소(HTML 태그)에 접근 가능
const ref = useRef(value)
// useRef() 함수는 value 값으로 초기화된 ref 객체를 반환
ref = { current : value }
// ref 값 변경
ref.current = "hello"
cf.) useState() vs useRef()
state와 ref의 초기값을 둘 다 0으로 만든 후, 버튼 클릭 시 값을 1씩 증가하게 하는 코드를 작성.
const [count, setCount] = useState(0);
const countRef = useRef(0);
function addStateHandler() {
console.log("STATE 변경");
setCount((prevState) => prevState + 1);
}
function addRefHandler() {
console.log(countRef.current);
countRef.current = countRef.current + 1;
}
useState는 값이 바뀌면 화면이 재렌더링이 되며 실제로 화면도 새로 갱신을 해준다. 하지만 useRef는 값이 바뀌어도 화면이 갱신이 되지 않는다. 화면 렌더링을 하지 않는다는 것.
컴포넌트 내부에서 자주 값이 바뀌지만, 화면 출력에는 영향을 미치지 않는 경우 useRef를 사용하는 것이 좋다.
cf2.) useRef() vs 일반 변수
useRef | 컴포넌트가 mount 되는 순간부터 unmount 되는 순간까지 값을 유지. 화면이 렌더링 될 때 바뀐 값이 표시된다.
일반 변수 | 값은 바뀌지만 useRef처럼 화면 갱신이 되지 않는다. useRef는 화면에 렌더링 될 때 바뀐 값이 표시된다면 일반 변수는 화면이 렌더링 될 때 (갱신하는 순간) 값이 초기값으로 초기화된다.
useRef()로 DOM에 접근하기
function App() {
const inputRef = useRef();
useEffect(() => {
console.log(inputRef);
// input 태그에 focus 설정
inputRef.current.focus();
}, []);
return (
<div>
ID : <input type="text" ref={inputRef} />
</div>
);
}
3. useEffect()
React의 LifeCycle을 제어하는 Hook
리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 수행하도록 설정해 주는 Hook으로 최초에 한 번만 실행하게 하고 싶은 작업을 작성할 때 주로 사용한다. ex.) fetch()를 이용한 네트워크 통신 연결
useEffect(() => callBackFunction, [deps]);
- 의존성 배열이 없는 경우
컴포넌트가 렌더링 될 때, 무엇이든 값이 바뀌었을 때 실행 (사용하는 의미가 없다.) - 의존성 배열이 빈 배열인 경우
컴포넌트가 처음 랜더링 될 때만 실행 - 의존성 배열에 값이 있는 경우
컴포넌트가 처음 렌더링 될 때, value 값이 변경되었을 때 실행. 의존성 배열에는 여러 값을 넣을 수 있다.
cleanup - 뒷정리하기
컴포넌트가 update 되기 직전 또는 unmount 되기 직전에 어떠한 작업을 수행하고 싶다면 뒷정리(cleanup) 작업을 해줘야 한다. useEffect() 함수 내부에서 return을 반환하는 식으로 cleanup을 수행.
ex.) socket 문 닫기 db 연결 해제하기 등
useEffect(() => {
console.log(`렌더링이 완료되었습니다`);
console.log({ names });
return () => {
// 예시는 console.log지만 일반적으로 연결을 해제하는 코드를 작성
console.log("cleanup");
console.log({ names });
};
}, [names]);
4. useReducer()
컴포넌트의 상태를 관리할 때 사용하는 Hooks
- 컴포넌트 상태 변경 로직을 컴포넌트에서 분리 가능
- 다른 컴포넌트에서도 해당 로직을 재사용 가능
useReducer() 사용 방법
const [state, dispatch] = useReducer(reducer, initialState);
state | 컴포넌트에서 사용할 상태 값 |
dispatch | 액션을 발생시키는 함수 |
reducer | reducer 함수 |
initialState | 초기 상태 값 |
dispatch() 사용 방법
dispatch({ action 객체 })
dispatch({ key : value })
// 사용 예시
dispatch({ type:"INCREMENT", data:initData });
action에 필요한 데이터의 값이 없다면 type만 넘겨주면 된다.
reducer()
현재 상태(state)와 action 객체(업데이트를 위한 정보)를 인자로 받아와서 새로운 상태를 반환해 주는 함수.
별도의 파일로 만들어서 관리한다. (export를 사용해서) 컴포넌트가 아니기 때문에 첫 글자가 대문자가 아니다.
default 값을 적어주지 않으면 warning이 뜬다.
export const reducer = (state, action) => {
switch (action.type) {
// newState 작성...
case "INCREMENT": {
return newState;
}
case "DECREMENT": {
return newState2;
}
default:
return state;
}
};
// 여러개의 reducer를 한 파일에 놔두고 export를 한다면 import는 아래와 같이
import { reducer } from 파일경로
dispatch와 reducer 사용하기
function rabbit() {
dispatch({ type: "INCREMENT", icon: "🐇" });
}
function turtle() {
dispatch({ type: "DECREMENT", icon: "🐢" });
}
return (
<div>
<p>{state}</p>
<button onClick={rabbit}>rabbit</button>
<button onClick={turtle}>turtle</button>
</div>
);
export function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return action.icon;
case "DECREMENT":
return action.icon;
default:
return state;
}
}
useState() vs useReducer()
useState()
- 컴포넌트에서 관리하는 값이 한 개
- 값이 단순한 숫자, 문자열, 불리언 등의 값인 경우
useReducer()
- 컴포넌트에서 관리하는 값이 여러 개
- 구조가 복잡한 경우
5. useMemo
컴포넌트 최적화를 위해 사용되는 Hook으로 동일한 계산을 하는 함수를 포함하고 있는 컴포넌트가 반복적으로 렌더링이 될 때, 해당 함수의 값을 메모리에 저장해 놓고 재사용할 수 있게 한다.
메모이제이션(Memoization)
- 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거
- 프로그램 실행 속도를 빠르게 하는 기술
useMemo()를 사용하는 이유
컴포넌트 내부에 하나의 함수가 있고, 함수의 출력값을 return으로 출력할 때, 렌더링이 될 때마다 컴포넌트가 호출되고 또다시 함수를 호출하여 계산이 된 후 화면에 출력되는 현상이 발생한다.
즉, 함수의 결과값이 초기화가 되어 다시 한번 값을 연산해야 하는 것이다.
useMemo()는 함수 내부의 로직이 바뀌지 않았다면, 결과 값이 바뀌지 않는 한, 함수의 결과값을 다른 메모리에 저장한 후 해당 값을 가져와 그대로 사용할 수 있게 해주는 것이다.
useMemo(() => callbackfunction, [deps])
첫 번째 매개변수 | 콜백함수. 메모이제이션 할 값을 계산해서 반환해주는 함수
두 번째 매개변수 | 의존성 배열. 배열 안의 값이 업데이트 될 때만 콜백함수를 재호출
useEffect와 사용 방법이 같다!
- 의존성 배열이 없는 경우
컴포넌트가 렌더링 될 때마다 콜백함수 호출. useMemo()를 사용하는 의미가 없다. - 의존성 배열이 빈 배열인 경우
컴포넌트가 마운트되었을 때만 콜백함수를 호출하고, 이후에는 항상 같은 값을 가져와서 사용한다. - 의존성 배열에 값이 있는 경우
의존성 배열의 값이 변화할 때만 콜백함수를 호출하여 값을 변경한다.
사용예시 (계산기)
컴포넌트의 외부에 함수 작성
const hardCalculate = (number) => {
console.log(`어려운 계산중`);
for (let i = 0; i < 1000000000; i++) {}
return number + 10000;
};
const easyCalculate = (number) => {
console.log(`쉬운 계산중`);
return number + 1;
};
컴포넌트의 내부에서 어려운 계산기를 사용하는 hardNum에 useMemo() 사용
function App() {
const [hardNum, setHardNum] = useState(1);
const [easyNum, setEasyNum] = useState(1);
const hardSum = useMemo(() => hardCalculate(hardNum), [hardNum]);
const easySum = easyCalculate(easyNum);
return (
<div>
<h3>어려운 계산기</h3>
<input
type="number"
value={hardNum}
onChange={(e) => setHardNum(parseInt(e.target.value))}
/>
<span> + 10000 = {hardSum}</span>
<hr />
<h3>쉬운 계산기</h3>
<input
type="number"
value={easyNum}
onChange={(e) => setEasyNum(parseInt(e.target.value))}
/>
<span> + 1 = {easySum}</span>
</div>
);
}
useMemo() 사용시 주의점
useMemo()의 사용 목적은 값을 재사용하기 위해 별도의 메모리를 할당하여 값을 저장하는 것이다. 만약 불필요한 값까지 메모이제이션하면 메모리 용량이 늘어나 오히려 성능 저하 발생.
6. useCallback
이미 생성해 놓은 함수를 재사용할 수 있게 해주는 Hook. useMemo()와 유사하다.
컴포넌트에 useCallBack()을 사용하지 않고 함수를 정의한 경우 렌더링이 발생할 때마다 함수가 새로 정의되는데, useMemo()의 경우 값을 저장하는것이라면 useCallBack()은 메모이제이션 된 콜백 함수를 다시 반환해준다.
const funcA = useCallback(() => 최적화 대상 함수, [deps])
- 의존성 배열이 없는 경우
컴포넌트가 렌더링 될 때마다 함수를 새로 생성. useCallback()를 사용하는 의미가 없다. - 의존성 배열이 빈 배열인 경우
컴포넌트가 마운트되었을 때만 함수를 새로 생성, 이후에는 항상 같은 함수를 가져와서 사용한다. - 의존성 배열에 값이 있는 경우
의존성 배열의 값이 변화할 때만 함수를 새로 생성한다.
자바스크립트 함수 동등성
const name1 = () => "soo";
const name2 = () => "soo";
둘의 값을 완전 비교(===)로 비교해 본다면 다른 값이라는 결과가 나온다. 이유는 함수는 객체라 다른 주소값을 가지고 있기 때문이다.
// clickHandler라는 함수가 바뀔 때만 재정의 하기 위해 작성하였지만 작동하지 않는 코드
useEffect(() => {
console.log(`clickHandler() 변경`);
}, [clickHandler]);
따라서 함수를 정의한 후, useEffect를 통해 의존성 배열에 해당 함수를 넣어도 state값이 변경될 때마다 함수가 재정의되며 주소값이 바뀌기 때문에 useEffect에 함수를 넣어도 의미가 없는 것이다. 이를 해결하기 위해 useCallback을 사용한다.
최종 코드
const clickHandler = useCallback(() => {
console.log(`count : ${count}`);
}, [count]);
useEffect(() => {
console.log(`clickHandler() 변경`);
}, [clickHandler]);
7. React.memo()
컴포넌트 재사용. 리액트에서 제공하는 고차 컴포넌트로 컴포넌트를 인자로 받아서 새로운 컴포넌트로 반환해준다.
porps의 변화가 있는지를 체크하며 변화가 있다면 렌더링 수행, 변화가 없다면 기존에 렌더링 된 내용을 재사용한다.
부모 컴포넌트가 리렌더되면 props를 가진 자식 컴포넌트 또한 리렌더가 된다. 따라서 props가 바뀌지 않는 한 리렌더링 되지 않았으면 하는 컴포넌트를 React.memo로 감싸주면 props가 바뀌지 않는 이상 리렌더링하지 않는다.
useCallback과 React.memo 사용
const updateHandler = useCallback(() => {
console.log("update");
}, []);
export const ChildComponent = React.memo((props) => {
const { updateHandler } = props;
console.log(`child component 렌더링`);
return <div></div>;
});
'새싹DT 기업연계형 프론트엔드 실무 프로젝트 과정 8주차 블로그 포스팅'