hook 함수란 무엇일까 ? 훅함수는 react의 재렌더링이 필요없는 컴포넌트의 렌더링을 방지하여 효율성을 올리는 역할을 하는 함수를 말한다.
우선 react에서 컴포넌트가 재렌더링 되는 경우는 다음과 같다
- 자신의 state가 변경될 떄
- 부모 컴포넌트로부터 전달받은 props가 변경될 때
- 부모 컴포넌트가 재렌더링 될 때
이는 react가 재렌더링을 시키는 조건이며, 해당 상황이 발생하면 자동적으로 렌더링이 일어난다. 하지만, 이 중에서는 필요없는 렌더링도 존재한다. hook함수는 state라는 상태로 관리하며, 꼭 필요할 때만 해당 컴포넌트
가 렌더링이 되도록 도와준다.
react는 hook이 호출되는 순서에 의존한다.
대표적인 hook에는 다음과 같이 5개이다.
- useState: 동적 상태 관리
- useEffect : side effect 수행
- useMemo : 연산한 값 재사용
- useCallback : 특정 함수 재사용
- useRef : Dom 선택, 컴포넌트 안에서 조회/ 수정할 수 있는 변수 관리
useState는 가장 기본적인 hook이며, 상태를 변경할 수 있는 훅함수이다.
const Counter = () => {
const [value, setValue] = useState(0);
return(
<div>
<p>현재 카운터의 값은 {value}입니다</p>
<button onClick={()=>{setValue(value+1)}}>+</button>
<button onClick={()=>{setValue(value-1)}}>-</button>
</div>
)
}
useState의 초기값으로는 위와 같은 숫자 뿐만아니라, 객체 배열등 다양하게 들어갈 수 있다. state는 useState에서 설정한 함수(setValue)로만 변경할 수 있다
또한 콜백에서 무거운 작업을 하는 값을 return문으로 넣으면 작업을 한번만 처리하여 효율성을 높일 수 있다
const heavywork = () => {
console.log('heavy work');
return ['a','b']
};
const Example1 = () => {
const [names, setName] = useState(()=>{
return heavywork();
})
}
useEffect는 컴포넌트의 생명주기에 맞춰 특정 작업을 수행할때 사용한다. 컴포넌트가 렌더링될때난 언마운트 될때, 특정 작업을 수행하도록 설정할 수 있는 Hook이다. useEffect는 개발 환경에서 컴포넌트가 화면에 나타날때 두 번 호출된다.
const Info = () => {
const [name, setName] = useState('');
const [nickname, setNickname] = useState('');
useEffect(()=>{
console.log('렌더링이 완료되었습니다');
console.log({name, nickname});
});
const onChangeName = () => {setName(e.target.value)};
const onChangeNickname = () => {setNickname(e.target.value)};
return( ...)
}
이 컴포넌트가 처음 나타날때 '렌더링이 완료되었습니다' 라는 문구가 두 번 출력된 것을 볼수 있다.
만약 마운트 될떄, 즉 처음 렌더링 될 때만 실행하고, 업데이트 될때 실행시키지 않으려면 두 번째 인수로 빈 배열을 넣어주면 된다.
useEffect(()=>{console.log("마운트 될때에만 실행된다"),[]});
특정 값이 업데이트될때만 실행하고 싶을 때는 두 번째 파라미터로 전달되는 배열안에 검사하고 싶은 값을 입력하면 된다.
다음 예제는 name이 변경될때만 실행된다.
useEffect(()=>{console.log(name);},[name]);
useEffect(()=>{
if(isNaN(inputText)=== true){
alert('숫자만 입력하세요');
setInputText("");
}
}, [inputText] );
위 예제의 경우 inputText값을 감지하여 그 값이 isNaN이면 alert메세지가 뜬다. 두번째 인수로 [inputText]를 넣었기 때문에, inputText가 변경될때마다 실행된다.
useEffect의 코드는 html 렌더링 이후에 동작하기 때문에 html렌더링 이후 해야 할 어려운 작업이나, 서버에서 데이터를 받아오는 작업, 타이머를 장착하는 작업 등을 할 떄 유용하다.
useEffect는 기본적으로 렌더링되고 난 직후마다 실행되며, 두 번째 파라미터 배열에 무엇을 넣는지에 따라 실행되는 조건이 달라진다.
동작 전에 특정코드를 실행하고 싶으면 return(()=>{})에 넣을 수 있는데 이를 clean-up함수라고 한다. useEffect를 시작하기 전에 깔끔하게 정리하고 시작하는 것이다.
다음은 setTimeout을 활용해 카운트 함수를 구현한 예제이다
useEffect(()=>{
let a = setTimeout(()=>{
setDiscount(false)},3000)
return()=>{
clearTimeout(a);
}
})
이 예제에서는 비동기로 실행되는 setTimeout이 들어가 있다. 이 함수를 사용해서 1회 실행되는 타이머를 생성한다. 이 타이머는 'a'라는 변수에 할당되어 컴포넌트가 생성될때마다 실행된다. 3초 뒤에 setDiscout라는 함수를 통해 Discount의 상태를 true에서 false로 만든다.
useEffect에서 clean-up 함수로 들어간 clearTimeout함수는 언마운트 될떄 'setTimeout'함수가 실행되지 않도록 타이머를 취소하는 역할을 한다. (원래라면 언마운트 될때에도 setTimeout 함수가 실행된다. ) 그러나 언마운트 될때에는 이 기능이 필요가 없기 때문에, clearTimeout 으로 이 기능을 취소해주는 것이다. 결과적으로는, 컴포넌트가 마운트되고 나서 3초 후에만 setDiscout(false)함수가 실행된다.
다음은 setInterval, useEffect를 사용해 타이머함수를 구현한 예제이다
//Timer.js
const Timer = (props) => {
useEffect(()) => {
const timer = setInterval(()=>{
console.log('타이머가 돌아가는 중');
},1000);
//clean-up함수, timer함수가 언마운트 될때 실행되는 뒷정리 함수이다.
return() => {
clearInterval(timer);
};
});
//실제로 보이는 html
return (
<div>
<span>타이머가 시작합니다. 콘솔을 보세요</span>
</div>
)
}
export default Timer;
다음은 위 파일을 import 한 App.js 파일이다.
//App.js
function App() {
const [showTimer, setShowTimer] = useState(false);
return (
<div className="App">
{/*showTimer가 true일때만 timer를 보여주고 아니면 안 보여준다*/}
{showTimer && <Timer />}
{/*버튼을 클릭하면 showTimer의 상태가 바뀐다*/}
<button
onClick={() => {
setShowTimer(!showTimer);
}}
>
Toggle Timer
</button>
</div>
);
}
위 예제에서는 timer가 1초마다 setInterval있는 콘솔을 실행해주고, 이 컴포넌트가 언마운트 될때 clearInterval이 작동하여 이 timer함수를 중단시킨다.
이 Timer라는 컴포넌트는 App에서 임포트 되며 button을 한번더 클릭하여 showTimer가 false가 되고 Timer가 {showTimer && } 조건식에 의해 없어질 때 언마운트 된다. 즉, 버튼을 클릭하면 이 clearInterval에 의하여 timer함수가 중단된다.
이렇게 useEffect를 사용하면 컴포넌트가 마운트, 언마운트 될때 특정 작업이 수행되도록 실행할 수 있다.
리듀서 함수는 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션 값을 전달받아 새로운 상태를 반환하는 함수이다. 이 함수에서 새로운 상태를 만들떄에는 반드시 불변성을 지켜야 한다. 이 함수는 나중에 배울 Redux라는 라이브러리와 밀접한 관련이 있다.
컴포넌트의 state를 변경하기 위한 행위를 useReducer를 거쳐 해야하는 것이다.
내가 은행에서 만원을 출금하는 행위를 useReducer로 예시를 든다면 다음과 같다.
1. 내가 만원 출금 요구 => Dispatch
2. '만원 출금'이라는 내용 => Action
2. 계좌내역 수정 => State
3. 계좌내역을 수정해주는 주체(은행) => Reducer
여기서 Action은 예금, 출금 등 다양한 형태의 요구사항이 가능하다.
Reducer가 Dispatch를 받으면 Action에 따라 State를 변경해준다!
쉽게 말하자면 reducer는 노예함수라고 생각하자
기본 형태는 다음과 같다.
function reducer(state,action){
return {...};
//불변성을 지키면서 업데이트한 새로운 상태를 반환한다.
}
다음은 useReducer로 카운터를 구현한 예제 코드이다.
import { useReducer } from "react";
function reducer(state, action) {
//action.type에 따라 다른 작업 수행
switch (action.type) {
case "Increment":
return { value: state.value + 1 };
case "Decrement":
return { value: state.value - 1 };
default:
//아무것도 해당되지 않으면 state 그대로 반환
return state;
}
}
//useReduce의 인자에는, reducer함수와 초기값을 넣어준다.
const Counter = () => {
//state는 현재 가리키고 있는 상태이고, dispatch는 액션을 발생시키는 함수이다.
//dispatch(action)과 같은 형태로, 함수안에 파라미터로 액션 값을 넣어 주면
//리듀서 함수가 호출되는 구조이다.
const [state, dispatch] = useReducer(reducer, { value: 0 });
return (
<div>
<p>
현재 카운터 값은 <b>{state.value}</b>입니다.
</p>
{/*해당 타입을 Increment로 액션을 dispatch하여
상태를 업데이트한다. */}
<button
onClick={() => {
dispatch({ type: "Increment" });
}}
>
+
</button>
<button
onClick={() => {
dispatch({ type: "Decrement" });
}}
>
-
</button>
</div>
);
};
export default Counter;
다음은 useReducer를 사용하여 여러 상태를 관리하는 방법이다. 컴포넌트가 렌더링될떄마다 상태를 계산하는 것이 아니라, 상태를 업데이트 할떄만 렌더링을 하는 방식이다. 따라서 useReducer는 컴포넌트가 불필요하게 렌더링되는 것을 방지하고 상태 업데이트 로직을 분리함으로써 코드의 가독성과 유지보수성을 향상시킨다.
import { useReducer } from "react";
function reducer2(state, action) {
return {
...state,
[action.name]: action.value,
};
}
const Info = () => {
//state의 초기값은 ""이다.
//dispatch함수를 사용하여 새로운 상태를 업데이트 할 수 있다.
//dispatch함수는 reducer함수에 전달할 action객체를 생성하고
//state객체를 업데이트한다.
const [state, dispatch] = useReducer(reducer2, {
name: "",
nickname: "",
});
//state객체는 name,nickname이라는 두개의 프로퍼티를 갖는다
//이 초기값은 위에서 지정하였다.
const { name, nickname } = state;
//onChange는 e.target으로 상태를 변화시킨다.
const onChange = (e) => {
dispatch(e.target);
};
return (
<div>
<div>
<input name="name" value={name} onChange={onChange} />
<input name="nickname" value={nickname} onChange={onChange} />
</div>
<div>
<div>
<b>이름 : </b>
{name}
</div>
<div>
<b>닉네임 :</b>
{nickname}
</div>
</div>
</div>
);
};
export default Info;
다음은 은행의 기능을 구현한 useReducer예제이다
import React, { useState, useReducer } from "react";
//Reducer - state을 업데이트하는 역할
//Dispatch - state를 업데이트 하는 요구
//Action - 요구의 내용
//객체로 빼주면 자동으로 수정이 된다.
const ACTION_TYPES = {
deposit: "deposit",
withdraw: "withdraw",
};
//reducer함수는 인자를 2개 받음
const reducer = (state, action) => {
console.log("reducer가 일을 합니다!");
switch (action.type) {
case ACTION_TYPES.deposit:
return state + action.payload;
case ACTION_TYPES.withdraw:
return state - action.payload;
default:
return state;
}
return state + action.payload;
};
const UseReducer1 = () => {
const [number, setNumber] = useState(0);
//useReducer는 useState와 비슷하나, 인자를 2개를 받는다.
//첫번째 인자는 reducer함수, 두번째 인자는 state인 money의 초기값이다.
const [money, dispatch] = useReducer(reducer, 0);
return (
<div>
<h2>은행</h2>
<p>잔고 : {money}원</p>
<input
type="number"
value={number}
onChange={(e) => setNumber(parseInt(e.target.value))}
//1000씩 증가하거나 감소함
step="1000"
/>
//dispatch를 할때 payload라는 데이터(짐)을 보낸다.
<button
onClick={() => {
dispatch({ type: ACTION_TYPES.deposit, payload: number });
}}
>
예금
</button>
<button
onClick={() => {
dispatch({ type: ACTION_TYPES.withdraw, payload: number });
}}
>
출금
</button>
</div>
);
};
export default UseReducer1;
dispatch를 사용할때 payload
라는 데이터를 보낼 수 있는데, 이는 useReducer함수에서 action.payload
사용할 수 있다.
둘다 상태를 업데이트하고 컴포넌트를 다시 렌더링하는 훅함수이다.
하지만 useState는 간단한 상태관리, useReducer는 복잡한 상태관리에 더 적합하다.
useState는 상태를 업데이트할때 새로운 상태를 직접 설정한다. setCount(count + 1) 같은 형식으로 직접 함수에 새로운 상태를 대입할 수 있다.
useReducer는 좀더 복잡한 상태를 관리할 때 사용한다. 상태 업데이트 로직이 별도의 리듀스 함수에서 처리되고, 해당 함수에서 반환된 새로운 값으로 업데이트 된다. useState처럼 새로운 상태를 직접 설정하지 않고, 액션(action)객체를 사용하여 새로운 상태 값을 계산하여 반환한다. 그 반환값이 새로운 상태가 된다.