함수형 컴포넌트의 이슈에 대해...
// 함수형 컴포넌트
export const Counter = () => {
const [count, setCount] = useState(0);
const [value, setValue] = useState(0)
const increment = () => {
setCount(count + 1);
console.log(`+`)
};
const decrement = () => {
setCount(count - 1);
console.log(`-`)
};
const view = () => {
console.log("실행");
return count;
};
return (
<>
<h2>{view()}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<h2>{value}</h2>
<button onClick={()=>{setValue(value+1)}}>+</button>
</>
);
};
// 클래스형 컴포넌트
class Counter2 extends React.Component {
state = { count: 0 };
increment() {
this.setState(this.tate.count + 1);
}
decrement() {
this.setState(this.tate.count - 1);
}
view() {
return this.state.count;
}
render() {
return (
<>
<h2>{this.view()}</h2>
<button onClick={() => this.increment()}>+</button>
<button onClick={() => this.decrement()}>-</button>
</>
);
}
}
클래스형 컴포넌트에서는 render()
함수 내에서 호출한 함수만 실행됩니다
반면 함수형 컴포넌트는 위 예제가 보여주듯이 컴포넌트의 상태가 바뀔 때(리렌더링)마다
그 안의 모든 함수가 의도치 않게 재실행된다는 문제가 있는데요
이로 인해 불필요한 리렌더링을 발생시킬 수 있습니다
리액트는 메모이제이션 기법을 활용해서 이에 대한 몇가지 해결책을 제공하고 있습니다
메모이제이션?
기존에 수행한 연산의 결과값을 메모리에 저장해두고, 동일한 연산이 필요한 경우
연산의 재실행 없이 저장된 값만 재활용하는 프로그래밍 기법입니다
useMemo()
는 메모이제이션된 값을 반환하는 함수입니다
useCallback(함수 값, [추적할 상태]) => 함수의 리턴값
예제1
const datetime = new Date().toISOString();
const today = useMemo(()=> {
return datetime
}, [count])
return <h2>{datetime}</h2>
상태가 바뀔 때마다 화면에 표시되는 시간이 달라집니다
예제2
import { useState, useMemo } from "react";
export const Memo = () => {
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
// 홀수만 구하기
const oddNumbers = useMemo(() => {
return numbers.filter((v) => v % 2 !== 0);
}, [numbers]);
const handleClick = () => {
setNumbers([...numbers, numbers.length + 1]);
};
return (
<div>
<p>숫자 : {numbers.join(", ")}</p>
<p>홀수 : {oddNumbers.join(", ")}</p>
<button onClick={handleClick}>요소 추가</button>
</div>
);
};
위 예제에서 oddNumbers
는 numbers
배열에서 홀수만을 필터링한 값으로,
배열이 업데이트될 때마다 매번 계산되어야 합니다
위와 같이 useMemo()
를 사용하면, numbers
배열이 업데이트되었을 때
이전에 계산된 oddNumbers
변수의 값을 재사용할 수 있기 때문에
매번 연산하는 비용을 줄일 수 있습니다
useCallback(함수 값, [추적할 상태]) => 함수 내용
useCallback()
은 결과값만 반환하는 useMemo()
와 달리, 상태가 변할 때마다 함수의 내용 전체를 반환합니다
리액트 컴포넌트에서 렌더링이 발생하면, 컴포넌트 내부의 모든 함수가 새로 생성됩니다
이는 상태 변경으로 인한 리렌더링에서도 마찬가지인데요
반면 useCallback()
은 이전 렌더링에서 생성된 함수를 기억하고, 다음 렌더링에서 같은 함수가 필요한 경우에는 이전에 생성된 함수를 재사용합니다
렌더링이 발생할 때마다 함수를 새로 생성하지 않아도 되기 때문에,
이로 인한 성능상 이점을 가져올 수 있다는 것이 핵심입니다
예제
import { useState } from "react";
export const ParentComponent = () => {
const [count, setCount] = useState(0);
const [value, setValue] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setValue(value + 1);
};
return (
<>
<h2>Count: {count}</h2>
<ChildComponent increment={increment}></ChildComponent>
<h3>{value}</h3>
<button onClick={decrement}>-</button>
</>
);
};
const ChildComponent = ({ increment }) => {
console.log("리렌더링");
return <button onClick={increment}>+</button>;
};
위 코드를 실행해보면 자식 컴포넌트와 무관한(프롭스를 전달하지 않은) -
버튼을 눌러도
자식 컴포넌트가 함께 리렌더링되는 것을 확인할 수 있습니다
useCallback()
과 memo()
를 사용해서 코드를 아래와 같이 수정해보겠습니다
import { useState, useCallback, memo } from "react";
export const ParentComponent = () => {
const [count, setCount] = useState(0);
const [value, setValue] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
const decrement = useCallback(() => {
setValue(value + 1);
}, [value]);
return (
<>
<h2>Count: {count}</h2>
<ChildComponent increment={increment}></ChildComponent>
<h3>{value}</h3>
<button onClick={decrement}>-</button>
</>
);
};
const ChildComponent = memo(({ increment }) => {
console.log("리렌더링");
return <button onClick={increment}>+</button>;
})
이제 -
버튼을 눌렀을 때(프로퍼티가 변하지 않았을 때)에는 자식 컴포넌트가 리렌더링되지 않습니다
+)
그런데 사실 소규모 프로젝트에서 useMemo
나 useCallback
사용으로 인한 최적화를 체감하기는 어렵습니다
그리고 useCallback
과 memo
함수 사용에도 나름의 비용이 발생하기 때문에, 반드시 더 효율적인 결과를 가져오는 것도 아닙니다
비동기 요청을 쓸 데 없이 재실행하는 경우처럼 리소스 낭비가 심한 게 아니라면 굳이 남용하지 않는 것이 좋다고 하네요
그럼에도 꼭 알아둬야 할 내용임에는 틀림없으니...
회원가입이 필요한 사이트에서 유저에 관한 상태는 모든 컴포넌트에서 공유할 필요가 있습니다
그런데 최상위 컴포넌트에서 최하위 컴포넌트까지, 상태가 프롭스를 타고 타고 내려가는 것은 너무 비효율적이겠죠
(이런 식의 사용을 Prop Drilling이라고 합니다)
이럴 경우 프로퍼티 전달을 사용하지 않고 모든 컴포넌트에서 관리하는 상태를 전역상태라고 하며,
useContext
는 이러한 전역상태를 관리하기 위해 사용합니다
사용 예제
import { useState, createContext, useContext } from "react";
// 전역상태를 생성합니다
const Global = createContext();
const D = () => {
const text = useContext(Global)
return <>Hello user : {text}</>;
};
const C = () => {
return <D></D>;
};
const B = () => {
return <C></C>;
};
const A = () => {
return <B></B>;
};
export const Context = () => {
const [user, setUser] = useState("alpha");
return (
<Global.Provider value="alpha">
<A></A>
</Global.Provider>
);
};
↓ 실행 결과
Hello user : alpha
그리고 관리할 데이터가 여럿이라면 객체를 활용하기
import { useState, createContext, useContext } from "react";
// 전역상태를 생성합니다
const Global = createContext();
const D = () => {
const obj = useContext(Global);
return (
<>
Hello user : {obj.user}
<button
onClick={() => {
obj.setUser("delta");
}}
>
rename
</button>
</>
);
};
const C = () => {
return <D></D>;
};
const B = () => {
return <C></C>;
};
const A = () => {
return <B></B>;
};
export const Context = () => {
const [user, setUser] = useState("alpha");
const initialState = {
user,
setUser,
};
return (
<Global.Provider value={initialState}>
<A></A>
</Global.Provider>
);
};
결과 rename 버튼을 누르면 모든 컴포넌트에서 유저에 관한 상태가 바뀐 것을 확인할 수 있습니다
reducer
함수는 현재 상태(state) 객체와 행동(action) 객체를 인자로 받아서,
새로운 상태(state) 객체를 반환하는 함수입니다
주로 상태 관리 로직을 따로 꺼내어 쓰기 위해 사용합니다
useReducer(함수값, {초기 상태값}) => [상태값, 상태를 바꾸는 함수]
reducer
가 넣어줍니다const initialState = {}
const reducer = (state) => {
console.log(state)
}
const [state, dispatch] = useReducer(reducer, initialState)
dispatch
라는 이름을 사용합니다 (setState, setValue... 와 같은 역할)dispatch()
가 실행되면 reducer()
함수를 호출합니다reducer()
가 발동되면 인자인 action값에 따라 새로운 상태를 객체 형태로 반환합니다action
{
type: [액션의 종류를 식별할 수 있는 문자열],
[액션의 실행에 필요한 임의의 데이터],
}
기본 예제
import { useReducer } from "react";
const reducer = (state, action) => {
console.log(state);
switch (action) {
case "increment":
return { count: state.count + 1, user: state.user };
case "decrement":
return { count: state.count - 1, user: state.user };
}
};
export const CounterReducer = () => {
const initialState = { count: 0 };
const [state, dispatch] = useReducer(reducer, initialState);
const increment = () => {
dispatch("increment");
};
const decrement = () => {
dispatch("decrement");
};
return (
<>
<h2>Count : {state.count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
};
위 예제에서 reducer
는 분기처리를 해서 코드를 실행하는 역할을 합니다
일반적으로는 아래 예제 형태를 따라 사용합니다
예제2
import { useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + action.payload, user: state.user };
case "decrement":
return { count: state.count - action.payload, user: state.user };
}
};
export const CounterReducer = () => {
const initialState = { count: 0 };
const [state, dispatch] = useReducer(reducer, initialState);
const increment = () => {
dispatch({ type: "increment", payload: 1 });
};
const decrement = () => {
dispatch({ type: "decrement", payload: 1 });
};
const handleSubmit = (e) => {
e.preventDefault();
const { counter } = e.target;
console.log(counter.value);
const action = {
type: "increment",
payload: parseInt(counter.value),
};
dispatch(action); // 1. type: 어떤 실행을 할 것인지, 2. payload: 바꿀 내용들
};
return (
<>
<h2>Count : {state.count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<br />
<form onSubmit={handleSubmit}>
<input type="text" id="counter" name="counter" />
<button>+</button>
</form>
</>
);
};
useReducer
에 대해서는 다음 포스트에서 더 자세히 다루기로 하겠습니다