[React] 리액트 필수 훅 예제로 알아보기
리액트 훅
Hook은 갈고리라는 의미이다. 리액트의 함수형 컴포넌트에서 상태(state)와 생명주기 메서드(lifecycle methods)를 관리하기 위한 방법을 제공한다. 원래 존재하는 기능(함수, 컴포넌트 등)에 갈고리처럼 끼어들어가 존재감을 뽐낸다고 하면 된다. 즉, 훅은 class, 함수를 따로 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해준다.
기본 Hooks
- useState : 상태를 관리하기 위한 훅, 즉 변수 값을 관리한다.
- useEffect : 사이드 이펙트를 처리한다. React에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 정의한다.
- useContext : 컴포넌트 트리 상에서 전역적인 데이터를 공유하기 위한 훅이다.
추가 Hooks
- useReducer : 상태 관리
- useCallback : 특정함수 재사용
- useMemo : 연산한 값 재사용
- useRef : 값 참조. Resource 제어
기본 훅 사용 예제
UseState
useState는 쉽게 말해 변수이다. []내에 값을 넣는데, 첫 번재 값은 state(변수), 두 번째 값은 해당 변수 값을 변경시켜주는 set함수이다.
const [count, setCount] = useState()//getter, setter라고 생각하면 된다.
위에서 count는 상태 변수, setCount는 해당 변수 값을 설정하는 역할을 한다.
아래의 형태를 이용하면 초기 값을 줄 수도 있다. 초기 값은 처음 렌더링 시에만 적용되며 이후에 값이 변경되면 무시된다.
const [count, setCount] = useState(1)
이 초기값은, 일반 타입 변수, 배열, 객체, 심지어 함수도 전달 가능하다.
interface prop{
a:number,
b:number
}
function App({a = 3, b = 2}:prop) {
const [val, setVal] = useState<number>(():number=>{
let result:number = a + b;
return result;
});
console.log(a)
return (
<div>{val}</div>
)
}
위의 코드는 프롭으로 받아온 a, b를 더해서 초기값으로 가지는 예제이다.
useState의 set함수는 기본적으로 비동기적으로 동작한다. 즉, set함수를 사용한 후 바로 비교 연산등을 사용하면 해당 값이 변경된 값을 가질 것이라는 기대를 가지기 힘들다는 것이다.
아래 코드는 버튼을 누를 때마다 count를 1씩 추가하는 일반적인 예제이다.
function App() {
const [count, setCount] = useState(0)
useEffect(()=>{
console.log(count);
}, [count])
return (
<>
<button onClick={()=>{
let newCount = count+1;
setCount(newCount)
}}>{count}</button>
</>
)
}
setState의 경우 일반적으로 값을 변경시키는 것이 아닌 새 것으로 갈아끼운다. 이게 무슨 소리냐. setCount를 사용하기 전의 count와 후의 count가 완전히 다르다는 것이다. (추후 useEffect 설명에서 다시 설명하겠다.)
아래의 경우 변경이 일어날 때마다 state값을 변경한다.(즉 인풋의 값이 그대로 적용된다.) 타입 스크립트 코드이다.
function App() {
const [message, setMessage] = useState<string>("")
console.log(message);
return (
<>
<input type="text" onChange={(e:React.ChangeEvent<HTMLInputElement>)=>setMessage(e.target.value)}/>
</>
)
}
useState는 변화(래퍼런스가 다름)가 발생할 때 리랜더링을 발생시키므로 아래의 경우 리랜더링이 되지 않는다.
interface notRandering{
keyOne:number
}
function App() {
const [val, setVal] = useState<notRandering>({keyOne:0});
const handleClick = ()=>{
console.log("Click!!");
let newVal:notRandering = val;
newVal.keyOne = val.keyOne + 1;
setVal(newVal);
}
console.log(val)
return (
<>
<button onClick={handleClick}>Btn</button>
</>
)
}
즉, console.log(val)가 초기 한 번 실행된 후, 버튼을 눌러도 다시 실행되지 않는다.
이때 깊은 복사(새 객체 할당, 주소가 달라짐)을 하면 리랜더링이 일어난다.
interface Raindering{
keyOne:number
}
function App() {
const [val, setVal] = useState<Raindering>({keyOne:0});
const handleClick = ()=>{
console.log("Click!!");
let newVal:Raindering= val;
newVal.keyOne = val.keyOne + 1;
setVal({...newVal});
}
console.log(val)
return (
<>
<button onClick={handleClick}>Btn</button>
</>
)
}
위의 경우 스프레드 연산을 통해 객체 내부의 값을 가지는 새로운 객체를 set으로 넣었다. 즉 새로운 객체이다. 다시 말해 useState의 리랜더링 조건인 변경이 일어난 것이다. 따라서 console.log(val)이 버튼을 누를 때마다 실행되는 것을 확인할 수 있다.
useEffect
유즈 이펙트는 사이드 이펙트를 관찰하고, 그에 따른 결과를 정의한다고 할 수 있겠다. 두번째 인자로 배열을 넘겨줄 경우, 해당 배열의 값(reference)가 변경될 경우 useEffect가 재실행된다.
useEffect의 경우 기본적으로 처음 페이지가 랜더링 되었을 때 한 번 실행된다. 그 이후에는 의존성 배열 내의 값이 변경된 경우에 재실행 되는 방식으로 작동된다.
useEffect(()⇒{
console.log("use Effect 실행");
}, [cnt])
위 useEffect훅은 cnt값이 변경될 때마다 재실행 된다.
만약 useEffect내에서 값을 변경시키고 해당 값을 의존성 배열에 넣을 경우 무한히 값이 변경 되는 무한 루프에 빠질 수 있다. 주의.
useEffect와 얕은 복사
import { useEffect, useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
function App() {
interface cnt{
count:number
}
const [count, setCount] = useState<cnt>({count:0})
useEffect(()=>{
console.log(count);
}, [count])
return (
<>
<button onClick={()=>{
let newCount:cnt = count;
newCount.count = count.count +1;
setCount(newCount)
}}>{count.count}</button>
</>
)
}
export default App
위의 경우 얕은 복사(객체의 래퍼런스/주소)가 그대로 newCount라는 변수로 복사 되었다. 그리고 그 변수를 set으로 처리하였다. 즉 이전과 같은 래퍼런스가 넘어가게 되어 useEffect는 변화가 일어나지 않은 것으로 인식된다.
UseContext
useContext는 쉽게 말해 전역으로 사용할 값을 관리한다. 이를 사용하지 않을 시 계층 구조가 3단계만 되어도 벌써 2번의 프롭 전달이 이루어져야한다. 즉, 코드가 굉장히 복잡해지며 예측하기 힘들게 된다.
import { createContext, useContext } from 'react'
import './App.css'
const SampleContextObj = createContext<string>("");
function UserComponent(){
const msg =useContext(SampleContextObj);
return(
<>
<h1>{msg}</h1>
</>
)
}
function App(){
/*useContext:전역변수*/
const message:string = "Hello World";
return (
<SampleContextObj.Provider value={message}>
<UserComponent/>
</SampleContextObj.Provider>
)
}
export default App
위 코드는 UserComponent라는 컴포넌트에 App에서 만든 변수 값인 “Hello World”를 넘겨준다.
우선 createContext로 생성된 객체 이름으로 값을 전달할 태그들을 감싸준다. 그리고 value로 실제로 설정될 값을 지정한다. 즉, <SampleContextObj.Provider value={message}>의 형태이다.
사용은 createContext객체를 useContext()의 파라미터로 넣으면 저장된 값을 반환해준다. 즉, const msg =useContext(SampleContextObj);의 형태이다.
바로 이어진 자식이 아닌 더 자손 요소에서도 값을 받아올 수 있다.
App.tsx
import './App.css'
import First from './First'
//First -> Third로 값 전달
function App(){
return (
<div>
<First/>
</div>
)
}
export default App
First.tsx
import {createContext} from 'react'
import Second from './Second'
export const TextContext = createContext<string|null>(null);
export default function First(){
const message:string = "Hello Hi Welcome~~~!!";
return(
<div>
<h2>First Componet</h2>
<TextContext.Provider value={message}>
<Second/>
</TextContext.Provider>
</div>
)
}
Second.tsx
import Third from './Third'
export default function Second(){
return(
<div>
<Third></Third>
</div>
)
}
Third.tsx
import {useContext} from 'react'
import { TextContext } from './First'
export default function First(){
const msg:string|null = useContext(TextContext);
return(
<div>
<h3>Third Component</h3>
<p>{msg}</p>
</div>
)
}
useMemo
값을 재사용한다. 이미 계산한 연산 결과를 기억해 두었다가 동일한 계산을 시키면 다시 연산하지 않고 기억해 두었던 데이터를 반환하는 것이다.
useEffect
는 해당 컴포넌트의 렌더링이 완료된 후에 실행되고, useMemo
는 렌더링 중에 실행된다는 차이점을 가진다. 그러나 useMemo를 사용하는 주요한 이유는 불필요한 랜더링을 방지하여 성능의 향상시키는 것이라고 할 수 있겠다.
아래의 경우를 보자
import './App.css'
import {useState, useMemo} from 'react'
const square = (params:number) => {
console.log("square 실행");
return params*params;
}
function App(){
const [countA, setCountA] = useState<number>(0);
const [countB, setCountB] = useState<number>(0);
function countResultA(){
console.log("A함수 실행");
setCountA(countA+1);
}
function countResultB(){
console.log("B함수 실행");
setCountB(countB+1);
}
const countSum = useMemo(()=>square(countB), [countB]);
//const countSum = square(countB);
return (
<div>
<p>
계산 결과 A : {countA}
<button onClick={countResultA}>A + 1</button>
</p>
<p>
계산 결과 B : {countB}
<button onClick={countResultB}>B + 1</button>
</p>
<p>{countA} + square({countB}) = {countA + countSum}</p>
</div>
)
}
export default App
위에서 useMemom는 의존성 배열의 값이 변경되면 함수를 실행시켜 그 값을 반환한다. 만약
const countSum = useMemo(()=>square(countB), [countB]);
//const countSum = square(countB);
이 부분에서 주석을 서로 바꾸면 일반 함수이기 때문에 A버튼을 누르더라도 Square함수가 실행된다. 그러나 위 useMemo를 적용할 경우 B값이 바뀔 경우에만 Square함수가 적용된다. 즉 성능이 (조금이나마) 향상 될 수 있다는 의미를 가진다.
Ref