Hooks 는 리액트에 새로 도입된 기능으로, 함수 컴포넌트에서 사용 불가능한
생명주기 메소드의 한계점으로 인해 상태 관리 및 렌더링 이후
시점 컨트롤 등 다양한 문제를 해결하기 위해 만든 함수 집합을 의미한다..
그 중 useState는 가장 기본적인 hook 이며, 함수 컴포넌트에서도
상태를 관리할 수 있게 해준다..
컴포넌트가 렌더링 된 이후 특정 작업을 수행할 필요가 있다면 클래스형 컴포넌트에서는
componentDidMount 혹은 componenetDidUpdate 메소드를 이용하면 된다..
하지만, 함수형 컴포넌트 에서는 생명주기 사용이 불가능하다..
그렇기에 함수형 컴포넌트에서도 렌더링 된 이후 시점에 수행할 내용이 필요한 경우,
사용할 수 있는 기능을 제공하고 있고, useEffect 이다..
import { useEffect, useState } from "react"
const UseEffectMount = () => {
const [time, setTime] = useState(new Date().toLocaleTimeString());
useEffect(()=>{
console.log("마운트 시점 동작");
},
// [] // 두번째 인자로 빈 배열을 넣으면 업데이트 시점에는 동작하지 않고 최초 마운트 시에만 동작한다.
[time] // 대괄호 안의 값이 바뀔때마다 동작한다.
);
return(
<>
<button onClick={()=>setTime(new Date().toLocaleTimeString())}>
현재 시간 확인
</button>
<h1>{time}</h1>
</>
)
}
export default UseEffectMount;
useEffect 는 기본적으로 렌더링 직후와 업데이트 직후 호출된다..
컴포넌트가 마운트 해제되기 직전이나 업데이트 되기 전에
실행할 내용이 있다면 정리 (clean-up) 을 할 수 있다..
이전 effect 내용을 정리하고 난 뒤 새로운 effect 가 동작하도록 할 때 사용한다..
이전 effect 가 남아있는 상태에서 새로운 effect 가 발생하게 되면
마운트 해제가 일어나고 나서도 메모리 누수나 충돌이 발생할 가능성이 있다..
import { useEffect, useState } from "react";
const Timer = () => {
const [cont, setCont] = useState(new Date().toLocaleTimeString());
useEffect(()=>{
console.log("타이머가 시작됨..");
const timer = setInterval(()=>{
// console.log(new Date().toLocaleTimeString());
setCont(new Date().toLocaleTimeString());
}, (1000));
return (
()=>{
clearInterval(timer);
console.log("타이머 끗");
}
) // useEffect가 unMount될때 return이 동작함
}, []);
// return <h1>타이머를 시작합니다..</h1>
return (
<>
<h1>타이머를 시작합니다..</h1>
<h3>{cont}</h3>
</>
)
}
export const Container = () => {
const [isTrue, setIsTrue] = useState(false);
return (
<>
<button onClick={()=>{setIsTrue(!isTrue)}}>타이머 토글</button>
{isTrue && <Timer/>}
</>
)
}
import { useReducer, useState } from "react"
export const Counter = () => {
const [counter, setCounter] = useState(0);
// useState의 set은 자기자신의 현재 값을 콜백함수의 인자로 받아올 수 있다..
const plus = () => {
setCounter(r=>r+1);
}
const minus = () => {
setCounter(r=>r-1);
}
return (
<>
<h1>counter : {counter}</h1>
<button onClick={plus}>+1</button>
<button onClick={minus}>-1</button>
</>
)
}
const reducer = (state, action) => { // (state: reducer에 넣어준 state, action: dispatch 인자)
switch(action.type){
case 'DECREMENT' :
return {value: state.value-1};
case 'INCREMENT' :
return {value: state.value+1};
default :
return state;
}
}
const UseReducerBasic = () => {
const [state, dispatch] = useReducer(reducer, {value: 0});
// [상태, 상태에 변화를 줄 함수] = useReducer(함수, state(객체)) ->순서가 바뀜
return (
<>
<h1>count : {state.value}</h1>
<button onClick={()=>dispatch({type: "DECREMENT"})}>-1</button>
<button onClick={()=>dispatch({type: "INCREMENT"})}>+1</button>
</>
);
}
export default UseReducerBasic;
메모이제이션(memoization)
useMemo
import { useEffect, useMemo, useState } from "react";
// useMemo : 메모이제이션을 통해 비용이 큰 계산을 최적화 하거나,
// 참조(주소값)이 동일해야 하는 경우에 사용한다.
// 의존성이 변경되지 않으면 이전의 계산된 값을 재사용 하여 불필요한 렌더링을 방지한다.
export const LocationComponent = () => {
const [isKorea, setIsKorea] = useState(true);
const [number, setNumber] = useState(0);
console.log("렌더링");
// 1. 지역 변수에 문자열로 초기화 - 기본 자료형(state 변화 시 location 변화 없음)
// const location = isKorea? "한국":"일본";
// 2. 지역 변수에 객체로 초기화 - 객체의 주소값이 렌더링 시 마다 변화..(재할당)
/* const location = {
country: isKorea? "한국" : "일본"
} */
// 3. 지역 변수에 useMemo 의 반환값으로 초기화
const location = useMemo(()=>{
return {
country: isKorea?"한국":"일본"
};
}, [isKorea]);
useEffect(()=>{
console.log("useEffect 호출");
}, [location]);
return (
<>
<h2>지금 당신의 위치는?</h2>
<p>국가: {location.country}</p>
<button onClick={()=>setIsKorea(!isKorea)}>국가 변경</button>
<hr/>
<h2>좋아하는 숫자를 입력 해주세요</h2>
<input type="number" value={number} onChange={e=>setNumber(e.target.value)} />
</>
)
}
함수 자체를 자식에 props로 뿌려줄 때 사용
-> 부모에서 바뀌면 자식 모두에서 다시 렌더링되기때문에 useCallback으로 불필요한 렌더링을 막아줘야한다.
useMemo :
특정 값을 메모이제이션한다..
주로 연산이 비싼 값이나 복잡한 계산..
사용 목적은 값이 자주 계산되는 것을 방지하여 성능 최적화..
useCallback :
특정 함수를 메모이제이션한다.. 함수의 재생성을 방지한다..
자식 컴포넌트에 함수를 전달할 때 불필요하게 함수가 재생성되는 것을 방지한다..
단점 : useCallback 을 많은 함수에 남용하면 오히려 성능 저하가 발생할 수 있다..
메모이제이션 자체도 메모리 비용이 들기 때문..
함수의 재생성에 큰 비용이 없는 경우 굳이 사용할 필요는 없다..
import { useCallback, useEffect, useState } from "react"
export const CallbackComponent = () => {
const [size, setSize] = useState(200);
const [isDark, setIsDark] = useState(false);
const genSquareStyle = useCallback(() => {
return {
backgroundColor : "orangered",
width: size,
height: size,
}
}, [size]);
return (
<>
<div style={{backgroundColor:isDark?"black":"white"}}>
<input type="range" min="100" max="300" onChange={e=>setSize(parseInt(e.target.value))}/>
<button onClick={()=>setIsDark(!isDark)}>{isDark.toString()}</button>
<Square genSquareStyle={genSquareStyle}/>
</div>
</>
);
}
const Square = ({genSquareStyle}) => {
const [style, setStyle] = useState({});
useEffect(()=>{
console.log("style 변경");
// setStyle(genSquareStyle()); 둘다가능
setStyle(genSquareStyle);
}, [genSquareStyle]);
return <div style={style}></div>
}
useRef 를 사용하는 목적
import { useRef, useState } from "react";
export const UseRefCounter = () => {
console.log("useRefCounter 렌더링 됨..");
const [count, setCount] = useState(0);
let variable = 0;
const countRef = useRef(0);
const increaseCount = () => { // 실행 시 재렌더링
setCount(count+1);
};
const increaseVariable = () => { // 실행 시 콘솔만 찍히고 렌더링 안됨 / 렌더링 될 때마다 초기화
variable += 1;
console.log("variable : "+ variable);
};
const increaseCountRef = () => { // 실행 시 콘솔만 찍히고 렌더링 안됨 / 렌더링 다시 될 때 출력됨
countRef.current = countRef.current+1;
console.log("카운트Ref: " + countRef.current);
};
return (
<>
<h1>counter : {count}</h1>
<h1>variable : {variable}</h1>
<h1>countRef : {countRef.current}</h1>
<button onClick={increaseCount}>카운트증가</button>
<button onClick={increaseVariable}>variable 증가</button>
<button onClick={increaseCountRef}>카운트 Ref 증가</button>
</>
);
}
context는 react 컴포넌트 트리 안에서 전역적으로 데이터를 공유할 수 있도록 고안된 방법이다..
트리 구조가 복잡해질수록 하위 컴포넌트로 props를 전달하는 양이 많아지고, 그러면 유지보수와 코드 가독성에 악영향을 준다..
하지만 context 를 사용하면 중간 컴포넌트 들에게 props 를 넘겨주지 않아도 되고 유지보수도 수월해지게 된다..
단, context 를 사용하면 컴포넌트를 재사용하기 어려워지기 때문에 꼭 필요할 때 써야한다..
따라서 때에 따라서는 context 보다 props 가 더 간단한 해결책이 될 수 있다.
import { createContext, useContext, useState } from "react";
import { Styles } from "./style";
// 컨텍스트 생성 -> 별도의 저장소를 만듦..
const DarkModeContext = createContext(null);
// 전역적으로 공유하고 싶은 것 중 useState 가 아닌 것들은 createContext()에 넣어주면 된다. // 사용은 잘안함
const Header = () => {
const context = useContext(DarkModeContext);
const {isDark} = context;
return(
<header style={{
...Styles.header,
backgroundColor:isDark?"black":"lightgray",
color:isDark?"white":"black"
}}>
<h1>Welcome to useContext</h1>
</header>
);
}
const Content = () => {
const context = useContext(DarkModeContext);
const {isDark} = context;
return(
<div style={{
...Styles.content,
backgroundColor:isDark?"gray":"white",
color:isDark?"white":"black"
}}>
<p>내용입니다..</p>
</div>
);
}
const Footer = () => {
const context = useContext(DarkModeContext);
const {isDark, setIsDark} = context;
const toggleHandler = () => setIsDark(!isDark);
return(
<footer style={{
...Styles.footer,
backgroundColor:isDark?"black":"lightgray",
color:isDark?"white":"black"
}}>
<button onClick={toggleHandler}>{isDark?"Light Mode":"Dark Mode"}</button>
useContext App
</footer>
);
}
const Page = () => {
return (
<div style={{
...Styles.page
}}>
<Header/>
<Content/>
<Footer/>
</div>
)
}
const ContextContainer = () => {
const [isDark, setIsDark] = useState(false);
return (
// context이름.Provider 컴포넌트 : 자식 요소들(<Page/>)이 사용할 수 있도록 value에 전달할 값들을 담아줌.
<DarkModeContext.Provider value={{isDark, setIsDark}}>
<Page/>
</DarkModeContext.Provider>
)
}
export default ContextContainer;
useContext 의 단점..
성능 저하
Context 는 전역 상태를 공유하기 때문에 공유된 상태가 변경되면
그 상태를 구독하는 모든 컴포넌트가 리렌더링 된다.
만약 Context 를 많이 사용하고 그 상태들이 자주 변경되면,
불필요한 리 렌더링이 발생하여 성능이 저하될 수 있다..
컴포넌트 재사용성 감소
Context 에 의존하는 컴포넌트가 많아질 수록, 해당 컴포넌트는 그 Context 환경 안에서만 동작할 수 있다..
이런 컴포넌트는 독립적으로 재사용하기 어려워지며, 다른 부분에서 쓰기 어렵다..
구조적인 복잡성 증가
Context 를 많이 사용하면 프로젝트 구조가 복잡해진다..
여러 Context 가 중첩되면 코드의 가독성이 떨어지고 유지보수에 악영향을 끼친다..
많은 컴포넌트가 여러 Context 에 의존하면, 프로젝트의 복잡도가 급격히 증가한다..
-> 관리 x 공유 o 관리는 useState에서 상태 관리를 하지 않기 때문에 단순한 것을 넘기는것은 괜찮지만 복잡한 것을 넘기는것은 어려움
동일한 동작이 필요한 것들에 hooks를 만들어 공통으로 사용할 수 있다.
import { useState } from "react";
const useInput = () => {
const [value, setValue] = useState("");
const onChange = e => setValue(e.target.value);
return {value, onChange};
}
export const CustomHooks = () => {
const name = useInput();
const pass = useInput();
const email = useInput();
return (
<>
<label htmlFor="">이름 : </label>
<input type="text" value={name.value} onChange={name.onChange}/>
<br/>
<label htmlFor="">비밀번호 : </label>
<input type="password" {...pass}/>
{/* 속성이랑 키 값이 같을 때 {...pass} 전개연산자로 사용가능 */}
<br/>
<label htmlFor="">이메일 : </label>
<input type="email" {...email}/>
<h4>name : {name.value}</h4>
<h4>pass : {pass.value}</h4>
<h4>email : {email.value}</h4>
</>
)
}