[React] Hooks
Hook이 도입된 이유, 역사
- Hooks는 리액트 16.8v에서 새로 도입된 기능이다.
- 기존에 클래스형 컴포넌트에서 state를 사용할때, 컴포넌트간
복잡한 관계
로 인한 로직 재사용이 힘들고, - 이해하기 어렵고,
- 클래스형 컴포넌트에서 이벤트를 달아줄때,
this를 binding
해줘야하는 다양한 이슈들이 있었다. - 이를 해결하기 위해서 Hooks가 도입되었다.
- 함수형 컴포넌트에서 state를 사용한다는 것은 리액트가
함수형 프로그래밍
이 된다는 것을 의미한다.
Hooks는 recompose
에서 시작되었다. 이 라이브러리가 Hooks의 기본적인 개념들을 처음으로 다뤘는데, 이 라이브러리는 리액트 팀에의해 인수되었다.
useState
Hooks의 도입으로 함수형 컴포넌트
에서 state의 사용이 가능하다.
import { useState } from "react";
const Counter = () => {
const [value, setValue] = useState(0);
return (
<div>
<p>현재 카운터값은 {value}입니다.</p>
<button onClick={() => setValue(value + 1)}> + 1</button>
<button onClick={() => setValue(value - 1)}> - 1</button>
</div>
);
};
export default Counter;
useState를 import한다.
useState는 기본적으로 2개의 원소를 가지는 배열을 리턴한다.
첫 번째 원소는 state를 유지할 값
,
두 번째 원소는 값을 변경할 함수
이다.
비구조화 할당 문법
을 사용하면 좀 더 직관적으로 원소들을 변수에 할당할 수 있다.
여러개의 state 사용
- state를 유지해야하는 값이 여러개라면 useState를 여러개 사용하면 된다.
useEffect
-
useEffect를 사용하면 함수형 컴포넌트에서 side effect를 실행할 수 있다.
- 즉, useEffect는 클래스형 컴포넌트의
라이프 사이클 메서드
를 대체할 수 있다. componentDidMount, componentDidUpdate, componentWillUnmount
가 합쳐진 것으로 생각해도 좋다(리액트 공식문서)
- 컴포넌트가 첫 렌더링(
마운트
) 되었을때, 업데이트
되었을때,- 그리고 DOM에서 사라지기 전(
언마운트
)될 때를 관리할 수 있다.
useEffect는 기본적으로 콜백함수와 배열을 인자로 가지는데,
- 콜백함수는 useEffect가 호출되면 실행할 함수,
- 배열은 호출할때
감시할 state
이다
예시
useEffect(() => {
console.log("렌더링이 완료되었습니다.");
console.log(name, nickName);
});
이렇게 사용하는게 기본형태라고 볼 수 있는데, useEffect는 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행된다.
컴포넌트의 렌더링이 완료되었을때 기본적으로 1회 호출되며, 컴포넌트가 업데이트되어 다시 렌더링될때마다 호출되는 것을 볼 수 있다.
즉 2번째 인자가 없으면 컴포넌트의 state가 변할때마다
호출되므로 componentDidmount & componentDidUpdate
의 역할을 한다.
마운트 될때만 사용하기
useEffect를 componentDidMount
로 사용하고싶다면,
빈 배열을 2번째 인자로 부여한다.
useEffect(() => {
console.log("마운트시에만 실행");
console.log(name, nickName);
}, []);
두번째 인자에 빈 배열을 주면 감시할 state가 없으므로, 컴포넌트가 마운트될때 1번만 호출된다.
따라서 componentDidmount
의 역할만을 하게된다.
특정 값이 업데이트 될때만
useUpdate를 특정값이 업데이트 될때만
사용하고싶다면,
- 해당 state를 배열에 추가한다.
useEffect(() => {
console.log("name이 업데이트 되었습니다.");
}, [name]);
두번째 인자로 들어가는 배열의 인자로 state를 넣으면 첫 마운트에 기본적으로 실행된 이후,
해당 state가 변할때만 실행하도록하는 코드가 된다.
clean-up
위의 예시들과는 달리 정리가 필요한 effect가 있다.
clean-up은 해석하면 정리
인데, Unmount 되기전에 하는 작업
을 의미한다고 이해했다.
- setInterval처럼 주기적으로 호출하는 함수가 데이터를 다운로드한다고 생각할때,
-
다운로드된 데이터를 렌더링하는 컴포넌트가 사라졌는데도 계속 호출하면? 엄청난 메모리 누수가 생길 것이다.
-
clean-up은 라이프사이클 메서드 중,
componuntWillUnmount, componentWillUpdate
를 대체할 수 있다. - 사용법은 useEffect함수에
리턴함수
를 설정해주면 된다.
예시로 컴포넌트를 DOM상에서 사라지게하거나 생기게할 수 있는 버튼을 만든다고 생각해보자!
import React from "react";
import { useState } from "react/cjs/react.development";
import Counter from "./components/Counter";
import Info from "./components/Info";
const App = () => {
const [visible, setVisible] = useState(false);
const handleClick = () => {
setVisible(!visible);
};
return (
<div className="App">
<button onClick={handleClick}>{visible ? "숨기기" : "보이기"}</button>
{visible && <Info />}
</div>
);
};
export default App;
렌더링할 APP컴포넌트가 숨기기/보이기 버튼을 만드는데, visible state에 따라 Info
컴포넌트를 마운트하거나, 언마운트한다.
import React, { useState, useEffect } from "react";
const Info = () => {
const [name, setName] = useState("");
const [nickName, setNickname] = useState("");
useEffect(() => {
console.log("effect");
console.log(name);
return () => {
console.log("clean up");
console.log(name);
};
}, [name]);
const onChangeName = (e) => {
setName(e.target.value);
};
const onChangeNickname = (e) => {
setNickname(e.target.value);
};
return (
...)
};
export default Info;
Info 컴포넌트의 useEffect는 name state를 감시하고있다.
앱을 보면, 초기값으로 현재 Info 컴포넌트가 숨겨져있어 Mount되지 않았다.
-
그리고 버튼을 눌러 Info 컴포넌트를
마운트
시키면effect
만 콘솔에 출력되는 것을 볼 수 있다. -
그리고 다시 숨기면 Info 컴포넌트가
언마운트
되면서cleanup
되는 것을 볼 수 있다.
- 이 상태에서 이름을 입력하면 name의 상태가 바뀜에따라 effect가 출력되는데,
- 다음 값을 입력하면 cleanup함수가 먼저 호출된다.(name state가 변경되므로, cleanup먼저 실행)
useReducer
-
컴포넌트의 다양한 상황에 따라 다양한 상태를 다른 값으로 업데이트하고싶을때 사용하는 훅.
-
다수의 하윗값을 사용하거나, 다음 state가 이전 state에 의존적인 경우
useReducer를 선호한다. -
useReducer훅을 사용하면 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있는 장점이 있다.
useReducer로 카운터구현
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
const [state, dispatch] = useReducer(reducer, initialState);
- useReducer의 첫번째 인자로 reducer 함수를 넣고, 두번째 인자로 state의 기본값을 넣어준다.
- 그러면 useReducer는
state값
과state에 적용할 수 있는 reducer함수를 적용한 dispatch
라는 함수를 받아온다.
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
reducer는 현재상태
와 업데이트위한 필요한 정보를 담은 액션
값을 전달받아 새로운 상태를 반환하는 함수이다.
reducer또한 불변성을 지켜주어야한다.
<button onClick={() => dispatch({ type: "INCREMENT" })}> + 1</button>
dispatch함수에는 파라미터로 액션값을 객체형태로 넣어주면 reducer함수가 그 액션값의 타입에 맞게 호출된다.
useReducer로 인풋상태관리하기
리듀서를 이용하면 클래스형 컴포넌트에서 state를 여러개 관리하던 것과 비슷한 형태로 할 수 있다.
function reducer(state, action) {
return {
...state,
[action.name]: action.value,
};
}
const Info = () => {
const [state, dispatch] = useReducer(reducer, {
name: "",
nickName: "",
});
const { name, nickName } = state;
const onChange = (e) => {
dispatch(e.target);
};
return(
<div>
<div>
<input value={name} name="name" onChange={onChange}></input>
<input value={nickName} name="nickName" onChange={onChange}></input>
</div>
...
</div>)
- reducer함수는 리턴값으로 state를 업데이트하는데,
- dispatch로 action인자가 들어오면
불변성을 지키기위해
spread문법
으로 기존 state를 풀고,[action.name]: action.value
->e.target.name: e.target.value
로 기존 state를 덮어씌운다.
- dispatch로 action인자가 들어오면
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
-
메모이제이션된 값을 리턴한다.
-
함수와 dependecies 배열(감시할 값의 배열)을 넣어야한다.
평균값을 계산하는 예시를 만들자.
useMemo를 사용해야할때
import { useState } from "react/cjs/react.development";
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([]); //배열state
const [number, setNumber] = useState(""); //numberstate
const onChange = (e) => {
setNumber(e.target.value); //number state 변화
};
const onInsert = (e) => {
const nextList = list.concat(parseInt(number)); //불변성
setList(nextList);
setNumber("");
};
return (
<div>
<input value={number} onChange={onChange}></input>
<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;
- 리액트는 state가 변함에따라 컴포넌트를 다시 렌더링한다.
- 이 코드의 문제점은
onChange
에 의해number
state가 변함에따라 컴포넌트가 다시 렌더링되고,- 이에따라
getAverage(list)
가함수를 호출
한다는 것이다.
- 이에따라
- 값을 입력중임에도 불필요한 계산을 계속하기때문에, 이런 불필요한 메모리낭비를 막기위해서
useMemo
를 사용한다.
useMemo사용 예시
import { useState } from "react/cjs/react.development";
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([]); //배열state
const [number, setNumber] = useState(""); //numberstate
const onChange = (e) => {
setNumber(e.target.value); //number state 변화
};
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}></input>
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {avg}
</div>
</div>
);
};
export default Average;
onChange
에 의해 state가 일괄적으로 update된 후, avg가 호출되는데,-
state중 list를 감시할 값으로 두었기때문에,
list
state가 변경되었을때(업데이트됐을때
)만 getAverage함수를 호출해 계산한다. - state를 업데이트할때
불변성
을 지켜야한다고 배웠다. -
불변성
법칙을 준수한다는 것은,setList(nextList)
가 다른메모리값
을 만들어 내는 것이다. -
list의
메모리값이 그대로
라는 것은 개발자가 list state를 변경하지 않았으므로, useMemo는 기존에 연산된 값을 리턴할 것이다. - list의
메모리값이 다르다는 것은
onInsert
가불변성
법칙을 준수하면서 새로운 리스트를 반환했기 때문에- useMemo는 감시중인(의존성값인)
list
의 메모리값이 다르다는 것을 인식하고, getAverage를 호출할 것이다.
- useMemo는 감시중인(의존성값인)
useCallback
useMemo가 특정 값을 메모이제이션한다면
-
useCallback은 함수를 메모이제이션한다.
- 반복적으로 선언되는 함수를 메모이제이션하는 것인데,
함수를 선언하는 것 자체가 큰 메모리나 cpu리소스를 차지하는 작업은 아니다.
- 그럼에도 컴포넌트의 렌더링을 자주 해줘야하거나, 렌더링할 컴포넌트의 깊이가 깊어지면 이 부분을 최적화해줘야한다.
위 함수에서 이벤트함수인 onChange
나 onInsert
에 적용한다.
const onChange = useCallback((e) => {
setNumber(e.target.value); //number state 변화
}, []);
const onInsert = useCallback(
(e) => {
const nextList = list.concat(parseInt(number)); //불변성
setList(nextList);
setNumber("");
},
[number, list]
);
onChange
는 Number state의 현재값과는 상관없이 값을 덮어서 업데이트만
하기때문에 의존성배열(두번째인자)에 감시할 값이 필요하지 않다.
하지만 onInsert
는 current
number
와 list
를 기준으로 값을 설정하는 함수이므로 의존성배열에 number와 list를 필요로한다.
useRef
useRef는 2가지의 용도로 사용된다.
DOM에 접근하는 용도
input으로 focus가 자연스럽게 넘어가도록 할 때, ref를 DOM에 달아주었는데, useRef를 사용하면 더 쉽게 ref를 사용할 수 있다.
...
const Average = () => {
const [list, setList] = useState([]); //배열state
const [number, setNumber] = useState(""); //numberstate
const inputE1 = useRef(null);
...
const onInsert = useCallback(
(e) => {
const nextList = list.concat(parseInt(number)); //불변성
setList(nextList);
setNumber("");
inputE1.current.focus()
},
[number, list]
);
...
return (
<div>
<input value={number} onChange={onChange} ref={inputE1}></input>
...
)
export default Average;
inputE1
이라는 변수에 useRef를 선언한다.
ref를 달아줄 태그에 ref props로 inputE1
을 달아주고,
버튼이 클릭될때마다 onInsert
될때마다 input으로 focus가 자동으로 넘어가게끔 inputE1.current.focus()
를 달아준다.
로컬변수 사용
useRef는 로컬변수를 관리하는 훅
으로 쓰인다고하는데,
로컬변수란 컴포넌트 렌더링과 관계없이 사용할 변수
를 의미한다.
클래스형 컴포넌트에서는 가변값을 유지하기위해 인스턴스필드에서 선언했는데,
함수형 컴포넌트에서는 useRef()
를 통해 가변값을 유지한다.
useRef는 순수 자바스크립트 객체를 생성하기때문에 매번 렌더링 할 때,
동일한 ref객체
를 만들어준다.
컴포넌트에서 state로 선언된 변수는 세터함수를 호출하고, 렌더링된 이후로 상태를 조회할 수 있지만
useRef
로 관리되는 변수는 설정 이후바로 조회
할 수 있다.
이 변수로 setTimeout, setInterval로 만들어진 id나, scroll위치 등의 값을 관리한다.
useRef를 사용하는 방법으로는 useRef로 선언된 값을 바꾸고싶다면 .current
로 접근해 값을 조회하거나, 수정하면 된다.
- 단, .current로 값을 바꾼다고하더라도 리렌더링을 유발하지는 않는다.(당연하지,,state가 아니니깐…)
useRef로 관리하는 변수는 값이 바뀐다고해서 컴포넌트가 리렌더링되지 않는다.
import React, { useRef } from "react";
const RefSample = () => {
const id = useRef(1);
const setId = (n) => {
id.current = n;
};
const printId = () => {
console.log(id.current);
};
return <div>refsample</div>;
};
export default RefSample;
커스텀 Hooks 만들기
-
여러 컴포넌트에서 쓰이는 로직을 함수로 뽑아내기
- input은 value에 props로 전달된 state값을 업데이트하기위해 name을 주고,
-
onChange에서
setValue([e.target.name] : e.target.value)
이런 방식으로 value값을 업데이트했다. - 이 로직이 input에서 반복되는 것을 알고 있다면, 함수화해서 사용하는 것이 더 편하다. 이럴때
커스텀 Hooks
를 만든다.
input에서 쓰인 useReducer로직을 함수화한다.
import { useReducer } from "react";
function reducer(state, action) {
return {
...state,
[action.name]: action.value,
};
}
export default function useInputs(initialForm) {
const [state, dispatch] = useReducer(reducer, initialForm);
const onChange = (e) => {
dispatch(e.target);
};
return [state, onChange];
}
책 + 인터넷자료을 뒤져가면서 이해한걸 정리했는데, 아직 미숙한 것 같다.
Hook들이 컴포넌트 최적화에 초점이 맞춰져있는 만큼, 미니 프로젝트를 생성하고, 이를 최적화해가면서 사용하면서 공부하는게 더 좋을 듯 싶다.
댓글남기기