2021. 12. 11. 19:41ㆍReactJS/Information
Hook ?
React.js 의 Hook은 React.js 16.8에서 나온 기능이다.
Hook을 이용하여 기존 Class 바탕의 코드로 작성할 필요 없이 상태값과 여러 기능을 사용할 수 있다.
(즉, Class Component 에서 사용되던 Lifecycle, 여러 기능들을 Functional Componet에서도 사용할 수 있다.)
1. useState
- 간단하게 말하면 상태에 대해 저장을 하고, 관리를 할 수 있게 해주는 Hook이다.
사용시에 아래와 같이 사용되면 상태를 불러올 수도, 변경할 수도 있다.
다만 유의할 점은 setState(); 를 통해 값을 변경했을 때 바로 반영되는 것이 아니라, Component가 Rerendering 되면서 값이 다시 주입되는 것 뿐이다.
([React.js] Re rendering 조건 : 맨 마지막 줄 참고)
(useState Hook 동작 원리에 대해 자세히 알아보고 싶다면, jjunyjjuny 님께서 작성하신 내용을 보면 잘 알 수 있다. : [ React ] useState는 어떻게 동작할까)
useState Hook의 사용 방법은 아주 간단하다.
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState<number>(0)
const incrementCount = () => {
setCount(count + 1)
}
const resetCount = () => {
setCount(0)
}
return (
<>
<span>{count}</span>
<button onClick={() => { incrementCount() }}>Increment</button>
<button onClick={() => { resetCount() }}>Reset</button>
</>
);
}
export default App;
count 변수를 useState() Hook을 이용해 만들어주고 기본 값은 0으로 준다.
5번째와 8번째 라인에서 생성한 count의 상태를 조작하는 함수를 만들어주고, (함수 표현식과 선언식은 강요되지 않는다.)
해당 내용을 불러오기만 하면 된다.
그러면 버튼을 눌렀을 때 State의 값이 변경되므로 Component가 Rerendering이 될 것이고, 최신 상태가 보여지는 것을 볼 수 있다.
2. useRef
useRef는 사용할 수 있는 방법이 2개가 존재한다.
- 특정 DOM을 선택
- Component 안에서 관리할 수 있는 변수 만들기
특정 DOM을 선택하는 방법
import React, { useState, useRef } from 'react';
function App() {
const currentCount = useRef<HTMLSpanElement>(null);
const [count, setCount] = useState<number>(0)
const incrementCount = () => {
setCount(count + 1)
console.log(currentCount.current)
}
const resetCount = () => {
setCount(0)
}
return (
<>
<span ref={currentCount}>{count}</span>
<button onClick={() => { incrementCount() }}>Increment</button>
<button onClick={() => { resetCount() }}>Reset</button>
</>
);
}
export default App;
특정 DOM을 선택할 때에는 아래와 같이 사용하면 된다.
위 처럼 사용하면 ref={currentCount} 가 사용된 DOM 요소를 Rendering이 될 때 currentCount에 주입되게 된다.
사용을 할 때는 currentCount.current 로 사용하면 된다.
여기서 current Method는 현재 선택된 요소를 뜻한다.
Component 안에서 관리할 수 있는 변수 만들기
이 부분은 말로 표현하는게 좀 어려워 조금 더 자세히 설명해보도록 하겠다.
useRef(); 로 관리하는 변수는 변경이 된다고 해서 Component가 Rerendering 되지 않는다.
useState(); 를 사용할 때에는 상태를 바꾸는 함수를 호출하고 나서 그 다음 렌더링 이후로 업데이트 된 상태를 조회 할 수 있다.
하지만 useRef 로 관리하고 있는 변수는 설정 후 바로 조회 할 수 있다.
즉, "컴포넌트의 전 생명주기를 통해 유지되는 값이라는 의미이며, 순수한 자바스크립트 객체를 생성 및 유지" 이다.
그럼 useRef()를 사용하는 대표적인 예시중 Timer(Interval)에 관련된 Component를 작성해보도록 하겠다.
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
let intervalId: NodeJS.Timeout;
const startCounter = () => {
console.log("출발!");
intervalId = setInterval(() => setCount((count) => count + 1), 1000);
};
const stopCounter = () => {
console.log("멈춰!");
clearInterval(intervalId);
};
return (
<>
<span>현재 카운트: {count}</span>
<button onClick={startCounter}>자동증가 시작</button>
<button onClick={stopCounter}>자동증가 정지</button>
</>
);
}
export default Counter;
위 Component를 불러오고 자동증가 시작 버튼을 눌러보면 Count는 정상적으로 올라가고 있는 것을 확인할 수 있다.
근데 이제 그만 멈추고싶다면 ?
자동증가 정지 버튼을 눌렀을 때 clearInterval을 진행하기 때문에 우리는 정상적으로 멈출 것 이라고 예상하였으나, 그 예상과는 반대로 멈추지 않는걸 볼 수 있다.
왜 이러는걸까 ?
→ Functional Component가 Rerendering 될 때에는 변수 및 함수를 다시 초기화 하고 있는데 위 코드대로라면 intervalId에 setInterval()의 결과값인 ID를 넣었으나, Rerendering이 되면서 변수 intervalId가 없어졌기 때문.
이 증상을 해결하려면 6번째줄에 intervalId를 useRef()로 바꿔주면 된다.
import React, { useState, useRef } from "react";
function Counter() {
const [count, setCount] = useState(0);
console.log("렌더링 : ", count);
const intervalId = useRef<NodeJS.Timeout>();
const startCounter = () => {
console.log("출발!");
intervalId.current = setInterval(() => setCount((count) => count + 1), 1000);
};
const stopCounter = () => {
console.log("멈춰!");
if(intervalId.current !== undefined) {
clearInterval(intervalId.current);
}
};
return (
<>
<span>현재 카운트: {count}</span>
<button onClick={startCounter}>자동증가 시작</button>
<button onClick={stopCounter}>자동증가 정지</button>
</>
);
}
export default Counter;
위처럼 진행하고 다시 한번 실행해보면 기대값과 같이 잘 나오고 있음을 확인할 수 있다.
3. useEffect
useEffect Hook은 useState와 같이 매우 중요한 Hook중 하나이니 잘 이해하고 있어야 한다.
useEffect Hook이 담당하고 있는 기능은 Component의 Lifecycle이다.
(Lifecycle중 일부..)
- ComponentDidMount
: Component가 최초 Rendering 됐을 때 실행되는 Life cycle
- ComponentDidUnMount
: Component가 사라졌을 때 실행되는 Life cycle
- ComponentDidUpdate
: Component가 다시 Rendering 됐을 때 실행되는 Life cycle
먼저 Component가 최초 Rendering 됐을 때를 조작하는 방법은 아래와 같다.
ComponentDidMount
import React, { useState, useRef, useEffect } from 'react';
function App() {
const currentCount = useRef<HTMLSpanElement>(null);
const [count, setCount] = useState<number>(0)
const incrementCount = () => {
console.log("COUNT를 증가하자! 현재 COUNT : ", count)
setCount(count + 1)
if(count === 1) {
console.log("COUNT가 1이 되었습니다!")
}
}
const resetCount = () => {
setCount(0)
}
useEffect(() => {
console.log("Hello, App!")
}, [])
return (
<>
<span ref={currentCount}>{count}</span>
<button onClick={() => { incrementCount() }}>Increment</button>
<button onClick={() => { resetCount() }}>Reset</button>
</>
);
}
export default App;
Increment, Reset 버튼을 눌러 Component를 다시 Rendering 시켜도 20-22번째 라인인 "Hello, App!" 은 다시 나타나지 않는다.
useEffect Hook 뒤에 붙은 '[]' 에 대해서는 후술하도록 하겠다.
ComponentDidUnMount
(테스트를 하기 위해서 App Component에 있는 기능을 새로운 Component로 이동시켰다. - Counter.tsx Component)
App.tsx 를 아래와 같이 변경하였다.
5초 뒤에 저장되어있는 상태값을 false로 바꿈으로써 Counter Component를 더이상 표시하지 않겠다고 작성한 것이다.
App.tsx
import React, { useState, useEffect } from 'react';
import Counter from "./Counter"
function App() {
const [isVisible, setIsVisible] = useState<boolean>(true)
useEffect(() => {
setTimeout(() => {
setIsVisible(false)
}, 5000)
}, [])
return (
<>
{
isVisible ? <Counter/> : <></>
}
</>
)
}
export default App;
Counter.tsx
import React, { useState, useRef, useEffect } from "react";
function Counter() {
const currentCount = useRef<HTMLSpanElement>(null);
const [count, setCount] = useState<number>(0)
const incrementCount = () => {
console.log("COUNT를 증가하자! 현재 COUNT : ", count)
setCount(count + 1)
if(count === 1) {
console.log("COUNT가 1이 되었습니다!")
}
}
const resetCount = () => {
setCount(0)
}
useEffect(() => {
console.log("Hello, App!")
return () => {
console.log("Bye, App!")
}
}, [])
return (
<>
<span ref={currentCount}>{count}</span>
<button onClick={() => { incrementCount() }}>Increment</button>
<button onClick={() => { resetCount() }}>Reset</button>
</>
);
}
export default Counter;
(단순히 App.tsx에 있던 내용을 Counter.tsx Component를 만들어 이관 시켜준 것이니 따로 설명은 안하도록 하겠다.)
useEffect()에 추가된 내용을 보면, 분명히 위에서는 ComponentDidMount()를 관리하는 Hook이라 했는데 막상 추가된 소스를 보니 ComponentDidMount()를 관리하고 있는 useEffect() Hook에서 return () => { ... } 소스만 추가 된 것을 볼 수 있다.
위와 같이 작성을 하면 Component가 사라졌을 때를 컨트롤할 수 있다.
실제로 테스트해보면, 5초 뒤에 Bye, App! 이라는 Log가 찍히는 것을 볼 수 있다.
ComponentDidUpdate
특정 요소가 업데이트 되었을 때를 컨트롤할 수 있다.
위 소스에서 count State가 변경될 때 마다 "changed count..." 라는 것을 출력하려 한다면 아래와 같이 작성하면 된다.
import React, { useState, useRef, useEffect } from "react";
function Counter() {
const currentCount = useRef<HTMLSpanElement>(null);
const [count, setCount] = useState<number>(0)
const incrementCount = () => {
console.log("COUNT를 증가하자! 현재 COUNT : ", count)
setCount(count + 1)
if(count === 1) {
console.log("COUNT가 1이 되었습니다!")
}
}
const resetCount = () => {
setCount(0)
}
useEffect(() => {
console.log("Hello, App!")
return () => {
console.log("Bye, App!")
}
}, [])
useEffect(() => {
console.log("changed count...")
}, [count])
return (
<>
<span ref={currentCount}>{count}</span>
<button onClick={() => { incrementCount() }}>Increment</button>
<button onClick={() => { resetCount() }}>Reset</button>
</>
);
}
export default Counter;
useEffect() 에서 뒤에 '[]' 라고 썼던 거에 변수를 넣어주면 된다.
이를 Dependency List라고 부르며 이름 그대로 배열이기 때문에 여러개의 변수를 넣어줄 수도 있다.
ComponentDidMount Lifecycle을 이용할 때 Dependency List(이하 deps) 에 빈 배열을 넣어준 이유는, deps에 배열이 비어있다면 초기 실행될 때만 실행되기 때문이다.
(왜냐면 초기 렌더링 될 때 한번 호출을 진행하니..)
실제로 실행하고 결과를 보면 잘 작동하는 것을 볼 수 있다.
4. useReducer
Reducer는 현재 상태와 액션 객체를 받아 새로운 상태를 리턴 해주는 순수 함수이다.
이 Reducer를 직접 구현해서 IncrementCount, ResetCount 함수를 보기 좋게 관리해보도록 하겠다.
port React, { useRef, useEffect, useReducer } from "react";
type ActionType = {
type: string,
payload: number,
};
const __INCREMENT_COUNT__ = "INCREMENT"
const __RESET_COUNT__ = "RESET"
function counterReducer(state: number, action: ActionType) {
switch(action.type) {
case __INCREMENT_COUNT__:
return state + action.payload;
case __RESET_COUNT__:
return action.payload;
default:
return state;
}
}
function Counter() {
const currentCount = useRef<HTMLSpanElement>(null);
const [count, dispatch] = useReducer(counterReducer, 0)
const incrementCount = () => {
dispatch({
type: __INCREMENT_COUNT__,
payload: 1
});
}
const resetCount = () => {
dispatch({
type: __RESET_COUNT__,
payload: 0
});
}
useEffect(() => {
console.log("Hello, App!")
return () => {
console.log("Bye, App!")
}
}, [])
useEffect(() => {
console.log("changed count...")
}, [count])
return (
<>
<span ref={currentCount}>{count}</span>
<button onClick={() => { incrementCount() }}>Increment</button>
<button onClick={() => { resetCount() }}>Reset</button>
</>
);
}
export default Counter;
5. useContext
context 객체(React.createContext에서 반환된 값)을 받아 그 context의 현재 값을 반환한다.
context의 현재 값은 트리 안에서 이 Hook을 호출하는 Component에 가장 가까이에 있는 <MyContext.Provider>의 value prop에 의해 결정된다.
App.tsx
import React, { createContext } from 'react';
import Counter from "./Counter"
export const CounterContext = createContext({
parent: "App"
});
function App() {
const contextValue = {
parent: "App"
}
return (
<>
<CounterContext.Provider value={contextValue}>
<Counter/>
</CounterContext.Provider>
</>
)
}
export default App;
Counter.tsx
import React, { useRef, useEffect, useReducer, useContext } from "react";
import { CounterContext } from "./App";
type ActionType = {
type: string,
payload: number,
};
const __INCREMENT_COUNT__ = "INCREMENT"
const __RESET_COUNT__ = "RESET"
function counterReducer(state: number, action: ActionType) {
switch(action.type) {
case __INCREMENT_COUNT__:
return state + action.payload;
case __RESET_COUNT__:
return action.payload;
default:
return state;
}
}
function Counter() {
const currentCount = useRef<HTMLSpanElement>(null);
const [count, dispatch] = useReducer(counterReducer, 0)
const context = useContext(CounterContext)
const incrementCount = () => {
dispatch({
type: __INCREMENT_COUNT__,
payload: 1
});
}
const resetCount = () => {
dispatch({
type: __RESET_COUNT__,
payload: 0
});
}
useEffect(() => {
console.log("Hello, App!. im created by", context.parent)
return () => {
console.log("Bye, App!")
}
}, [])
useEffect(() => {
console.log("changed count...")
}, [count])
return (
<>
<span ref={currentCount}>{count}</span>
<button onClick={() => { incrementCount() }}>Increment</button>
<button onClick={() => { resetCount() }}>Reset</button>
</>
);
}
export default Counter;
Provider에서 제공 된 value를 하위 Component에서 불러와 쓸 수 있다.
6. useMemo
이 Hook은 성능최적화를 할 때 많이 사용되는 Hook이다.
useMemo() Hook의 특징은 "이전에 계산한 값을 그대로 사용한다" 라는 특징이 있다.
이 말이 무엇이냐면, useMemo() Hook을 사용한 변수는 특정 값이 바뀌었을 때만 다시 초기화하고, 그렇지 않을 경우에는 이전에 계산한 값을 그대로 사용한다는 것이다.
(이 말이 이해가 가지 않는다면 useState()를 설명할 때 쓰여있는 Rerendering 조건에 대해서 다시 보면 된다.)
이 Hook을 이용해 Component가 최초 Loading 된 시간을 저장하고자 한다.
import React, { useRef, useEffect, useReducer, useContext, useMemo } from "react";
import { CounterContext } from "./App";
type ActionType = {
type: string,
payload: number,
};
const __INCREMENT_COUNT__ = "INCREMENT"
const __RESET_COUNT__ = "RESET"
function counterReducer(state: number, action: ActionType) {
switch(action.type) {
case __INCREMENT_COUNT__:
return state + action.payload;
case __RESET_COUNT__:
return action.payload;
default:
return state;
}
}
function fetchStartTime() {
console.log("시작시간 구하는중...")
return new Date()
}
function Counter() {
const currentCount = useRef<HTMLSpanElement>(null);
const [count, dispatch] = useReducer(counterReducer, 0)
const context = useContext(CounterContext)
const incrementCount = () => {
dispatch({
type: __INCREMENT_COUNT__,
payload: 1
});
}
const resetCount = () => {
dispatch({
type: __RESET_COUNT__,
payload: 0
});
}
const startTime = useMemo(() => { return fetchStartTime() }, [])
console.log("start time :", startTime)
useEffect(() => {
console.log("Hello, App!. im created by", context.parent)
return () => {
console.log("Bye, App!")
}
}, [])
useEffect(() => {
console.log("changed count...")
}, [count])
return (
<>
<span ref={currentCount}>{count}</span>
<button onClick={() => { incrementCount() }}>Increment</button>
<button onClick={() => { resetCount() }}>Reset</button>
</>
);
}
export default Counter;
(사실 위 기능만 구현하고자 하면 let variable을 선언하고, ComponentDidMount Lifecycle에서 값만 넣어주면 되지만.. 예제니까 이런식으로 작성했다.)
48번째 라인에서 deps에 빈 배열을 선언했기 때문에 해당 변수는 Counter Component가 최초 로딩 되었을 때만 값을 변경한다.
만약 count가 변경될 때 마다 값을 업데이트 하고자 하면 deps에 [count] 를 넣으면 된다.
7. useCallback
useCallback은 useMemo와 개념이 매우 비슷한 Hook이다.
실제로 useCallback은 useMemo에서 파생되었다고 한다.
useCallback의 기능은 useMemo와 동일하지만, 다른 점은 함수를 리턴한다는 점이다.
import React, { useRef, useEffect, useReducer, useContext, useMemo, useCallback } from "react";
import { CounterContext } from "./App";
type ActionType = {
type: string,
payload: number,
};
const __INCREMENT_COUNT__ = "INCREMENT"
const __RESET_COUNT__ = "RESET"
function counterReducer(state: number, action: ActionType) {
switch(action.type) {
case __INCREMENT_COUNT__:
return state + action.payload;
case __RESET_COUNT__:
return action.payload;
default:
return state;
}
}
function Counter() {
const currentCount = useRef<HTMLSpanElement>(null);
const [count, dispatch] = useReducer(counterReducer, 0)
const context = useContext(CounterContext)
const incrementCount = () => {
dispatch({
type: __INCREMENT_COUNT__,
payload: 1
});
}
const resetCount = () => {
dispatch({
type: __RESET_COUNT__,
payload: 0
});
}
const getTime = useCallback(() => { return new Date()}, [])
const startTime = useMemo(() => { return getTime() }, [])
console.log("start time :", startTime)
useEffect(() => {
console.log("Hello, App!. im created by", context.parent)
return () => {
console.log("Bye, App!")
}
}, [])
useEffect(() => {
console.log("changed count...")
}, [count])
return (
<>
<span ref={currentCount}>{count}</span>
<button onClick={() => { incrementCount() }}>Increment</button>
<button onClick={() => { resetCount() }}>Reset</button>
</>
);
}
export default Counter;
Component 외부에 선언되어 있던 fetchStartTime() 함수를 Component 안으로 옮겼다.
(useCallback Hook을 사용하므로 Component가 다시 Rendering되면서 불필요한 초기화를 방지했기 때문에 Component 내부로 옮길 수 있었다.)
사실 올바른 예제는 아닌것 같으나, 개념만 이해하고 넘어가자.
8. Custom Hook
Custom Hook은 말 그대로 개발자가 Hook을 만드는 것이다.
React.js에서는 제공하는 Hook 외에도 개발자가 직접 Hook을 만들 수 있다.
Custom Hook은 반복되는 로직을 관리하기 쉬워 사용한다.
Custom Hook은 useReducer, useCallback.. 원하는 기능을 사용하여 로직을 구현해주면 된다.
(단, export할 function의 명은 반드시 앞에 'use' prefix를 붙여야한다.)
Reducer로 조작하고 있는 부분을 간단하게 Hook으로 구현해보겠다.
useCount.ts
import {useCallback, useReducer} from "react"
type ActionType = {
type: string,
payload: number,
};
const __INCREMENT_COUNT__ = "INCREMENT"
const __RESET_COUNT__ = "RESET"
function counterReducer(state: number, action: ActionType) {
switch (action.type) {
case __INCREMENT_COUNT__:
return state + action.payload;
case __RESET_COUNT__:
return action.payload;
default:
return state;
}
}
function useCount(initialCount: number) {
const [count, dispatch] = useReducer(counterReducer, initialCount)
const incrementCount = useCallback(() => {
return dispatch({
type: __INCREMENT_COUNT__,
payload: 1
});
}, []);
const resetCount = useCallback(() => {
return dispatch({
type: __RESET_COUNT__,
payload: 0
});
}, []);
return [count, incrementCount, resetCount] as const
}
export default useCount;
Counter.tsx
import React, {useCallback, useContext, useEffect, useMemo, useRef} from "react";
import {CounterContext} from "./App";
import useCount from "./useCount";
function Counter() {
const [count, incrementCount, resetCount] = useCount(0)
const currentCount = useRef<HTMLSpanElement>(null);
const context = useContext(CounterContext)
const getTime = useCallback(() => {
return new Date()
}, [])
const startTime = useMemo(() => {
return getTime()
}, [])
console.log("start time :", startTime)
useEffect(() => {
console.log("Hello, App!. im created by", context.parent)
return () => {
console.log("Bye, App!")
}
}, [])
useEffect(() => {
console.log("changed count...")
}, [count])
return (
<>
<span ref={currentCount}>{count}</span>
<button onClick={() => {incrementCount()}}>Increment</button>
<button onClick={() => {resetCount()}}>Reset</button>
</>
);
}
export default Counter;
마치며.
React.js를 이용해 코드를 작성할 때 상황에 맞는 Hook은 고를 수 있어도, 아직 최적화 작업을 하는 것은 쉽지 않다.
더 많은 Hook이 존재하니 React 공식 문서를 통해 알아가면 될 것 같다.
- (2021.12.15)
갑자기 모든 소스가 모두 검정색깔로 변경되어서 다시 작성했다.
'ReactJS > Information' 카테고리의 다른 글
[React.js] createPortal 사용하여 DOM을 원하는 요소 안으로 옮기 (0) | 2021.12.11 |
---|---|
[React.js] 최상위 API React.memo (0) | 2021.12.11 |
[React.js] Redux & Reducer (0) | 2021.12.09 |
[React.js] Re rendering 조건 (0) | 2021.12.09 |
[React.js] Class Component vs Functional Component (0) | 2021.12.04 |