강의 속도가 점점 빨라지기 시작했다. 하루에도 투두리스트를 5번씩 갈아엎는 것 같다. 오늘은 뭘 배웠지...useCallback
이랑 useMemo
, React.Memo
, useReducer
, Context API
를 배웠다. 그 중 useReducer
와 context API
를 복습해보라고 과제를 내주셨다.
Hook이란, React 16.8 버전부터 추가된 기능이다. 함수 컴포넌트에서 React 상태와 생명주기 기능을 연동할 수 있게 해주는 함수
useReducer
와 비슷한 useState
를 먼저 다시 복습해보자.
React에서 상태 관리를 위해 사용하는 기본적인 Hook이다. 현재 상태의 값을 제공하고, 상태 값을 업데이트 하는 함수를 반환한다.
const [state, setState] = useState(초기값);
컴포넌트의 현재 값은 state
변수에 들어있다. setState
는 상태를 갱신하기 위해 사용하는 함수이다. setState
함수가 호출되면 상태값이 업데이트되고, 컴포넌트가 리렌더링 된다.
import { useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
console.log("rerendered");
return (
<>
<h3>useState를 알아보자</h3>
<h1>Count : {count}</h1>
<button onClick={() => setCount((prev) => prev + 1)}>증가</button>
</>
);
}
export default App;
위 예시 코드에서 상태 변수 count와, 함수 setCount를 선언했다.
코드를 실행하고 버튼을 눌러보면, count 값이 버튼 클릭 때마다 하나씩 증가하는 것을 확인할 수 있다. 콘솔 로그를 확인해보면, count 값이 하나 증가할 때마다 리렌더링 되는 것도 확인할 수 있다.
⭐정리
useState
를 사용하여 컴포넌트 내에서 상태를 쉽게 관리할 수 있고, 상태가 변경될 때마다 컴포넌트를 리렌더링하여 상태 값이 반영된 UI를 제공할 수 있다. 이를 활용하여 React 어플리케이션을 동적이고 반응적으로 구현할 수 있다!🚫주의
useState를 사용하면 state가 변경될 때마다 다시 렌더링되는데, 불필요한 렌더링을 일어나게 한다. 또, 한 번 리렌더링 될 때마다 콘솔 로그가 두 번씩 찍히는게 이는 React.StrictMode 때문이다. 오류가 아니라 정상적인 현상이니 놀라지 말기
useState
와 같은 상태를 관리하는 훅이다. 복잡한 상태 관리 로직을 처리하기 위해 사용한다.
const [state, dispatch] = useReducer(reducer, initialArg, init);
기본 형태는 위와 같다.
현재의 상태 값과, 상태를 업데이트하고 리렌더링을 유발하는 함수를 반환한다.
useReducer
선언 시 첫 번째 인자로 reducer
함수를 넘겨준다. reducer
함수는 state와 action, 두 개의 매개 변수를 가진다.
const reducer = (state, action) => {}
새로운 상태 값을 반환한다.
useReducer
은 dispatch
함수를 반환한다. action 객체를 매개변수로 받아서 reducer
함수로 전달한다.
위에서 useState
를 사용하여 구현한 카운터를 useReducer
을 사용한 형태로 변경해보았다.
감소와 초기화 기능을 추가했는데, 이처럼 상태의 업데이트에 관련된 별도의 기능을 reducer 함수에 집중시켜 한번에 처리할 수 있다.
import { useReducer } from 'react';
import './App.css';
//리듀서 함수를 정의
const reducer = (state: number, action: { type: string; payload?: number }) => {
if (action.type === 'increment' && action.payload)
return state + action.payload;
else if(action.type === 'decrement' && action.payload)
return state - action.payload;
else if(action.type === "reset")
return 0;
else return state;
};
function App() {
//useReducer를 사용하여 상태와 디스패치를 초기화한다
const [state, dispatch] = useReducer(reducer, 0);
return (
<>
<h3>useState를 알아보자</h3>
<h1>Count : {state}</h1>
<button onClick={() => dispatch({ type: 'increment', payload: 1 })}>
증가
</button>
</>
);
}
export default App;
useReducer
을 사용해서 상태 값 state
와 dispatch
를 초기화한다. 초기 상태 값은 0이다.dispatch
함수가 호출되고, action 객체 {type: "increment", payload : 1}
가 reducer
로 전달된다. 상태 값이 payload
에 설정한 만큼 증가한다.useReducer
을 이해하기 위해 다른 기능을 만들어 보았다. 숫자는 이제 질리도록 올리고 내려봐서 boolean을 활용한 기능을 구현해 보았다.
import { useReducer } from 'react';
import './App.css';
const reducer = (state: boolean, action: { type: string }) => {
if (action.type === 'toggle') return !state;
else return state;
};
function App() {
const [isOn, dispatch] = useReducer(reducer, false);
return (
<>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<h3>On, Off 토글해보기</h3>
<div
style={{
width: '50px',
height: '50px',
backgroundColor: isOn ? 'yellow' : 'black',
marginBottom: '30px',
borderRadius: '100px',
boxShadow: isOn ? '0 0 40px' : 'none',
}}
></div>
<button onClick={() => dispatch({ type: 'toggle' })}>불켜기</button>
</div>
</>
);
}
export default App;
버튼을 누르면 on, off 되는 간단한 코드이다.
useReducer
을 사용하여 isOn과 dispatch를 초기화 하였다. 초기 값은 false이다.
근데 useReducer
의 장점은 여러 기능을 reducer
내에서 집중 처리할 수 있다는 것인데, 단순 onoff만 할거면 useReducer
을 사용하는 의미가 없다. 기능을 조금 더 추가해보자
import { useReducer } from 'react';
import './App.css';
type Action =
| { type: 'toggle' }
| { type: 'setColor'; color: string }
| { type: 'setShape'; shape: string };
type State = {
isOn: boolean;
color: string;
shape: string;
};
const init: State = {
isOn: false,
color: 'black',
shape: '100px',
};
const reducer = (state: State, action: Action) => {
if (action.type === 'toggle')
return {
...state,
isOn: !state.isOn,
color: state.isOn ? 'black' : 'yellow',
shape: state.isOn ? '100px' : '100px',
};
else if (action.type == 'setColor') return { ...state, color: action.color };
else if (action.type == 'setShape') return { ...state, shape: action.shape };
else return state;
};
function App() {
const [light, dispatch] = useReducer(reducer, init);
return (
<>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<h3>내마음대로 만드는 전구(?)</h3>
<div
style={{
width: '50px',
height: '50px',
backgroundColor: light.color,
marginBottom: '30px',
borderRadius: light.shape,
boxShadow: light.isOn ? '0 0 40px' : 'none',
}}
></div>
<button onClick={() => dispatch({ type: 'toggle' })}>불켜기</button>
<button onClick={() => dispatch({ type: 'setColor', color: 'red' })}>
색바꾸기
</button>
<button onClick={() => dispatch({ type: 'setShape', shape: '10px' })}>
모양바꾸기
</button>
</div>
</>
);
}
export default App;
isOn
, color
, shape
를 포함한 상태를 정의한 State 타입을 정의하였고, 초기 상태 init
을 정의하였다.reducer
함수에서 액션을 처리하여 상태를 업데이트 한다.isOn
을 반전시킨다.reducer
에서 집중적으로 처리하여, 버튼 클릭 이벤트에 따라 상태를 업데이트하여 UI를 변경한다!⭐정리
useReducer
을 사용하여 복잡한 상태 로직을 단순화하고, 상태 변경 로직을 한 곳에서 관리할 수 있다. 가독성과 유지보수성이 향상된다!독립적이고 단순한 로직 처리에 적합한 useState, 복잡하고 의존적인 상태 관리에 적합한 useReducer
props를 사용하지 않고 필요한 데이터를 쉽게 공유할 수 있게 해준다.
이렇게 컴포넌트가 연결되어 있을 때, 데이터를 요하는 컴포넌트가 가장 하위에 있어도 최상위 컴포넌트부터 순차적으로 값을 전달 해주어야 한다. Props Drilling
, 프롭스 드릴링이라고 한다.
해당 값을 중간 컴포넌트가 사용하지 않아도, 전달을 위해 값을 받고, 전달해주어야 한다.
중간 컴포넌트에서 불필요한 값을 받는다는 점, 계층 간에서 이름이 변경되면 값을 추적하고 업데이트하기 번거로운 등 단점이 존재한다.
이를 해결하기 위한 방법 중 하나가 Context API
이다. Context를 생성하고 값을 제공하는 컴포넌트를 만들고, 필요한 컴포넌트에서 useContext
를 사용하여 해당 값을 직접 접근하여 사용한다. 중간 컴포넌트를 거치지 않는다!
import { createContext, useState } from "react";
import Page from "./components/learn/Page";
export const counterContext = createContext<{
count: number;
setCount: React.Dispatch<React.SetStateAction<number>>;
}>({ count: 0, setCount: () => {} });
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<counterContext.Provider value={{ count, setCount }}>
<Page />
</counterContext.Provider>
</>
);
}
import { useContext } from "react";
import { counterContext } from "../../App";
const DisplayCounter = () => {
const { count } = useContext(counterContext);
console.log("display counter");
return (
<>
<h1>Counter: {count}</h1>
</>
);
};
export default DisplayCounter;
Context API를 사용하는 코드이다.
createContext
를 사용하여 context를 생성한다.Provider
로 대상 컴포넌트를 감싼다.Provider
의 value
에 전달할 데이터를 넣는다.(위 코드에서는 count와 setCount를 전달한다)위 코드를 실행하면 < <counterContext.Provider value={{ count, setCount }}>...</counterContext.Provider>
사이에 있는 모든 컴포넌트가 리렌더링 된다. 데이터를 전달받지 않는 컴포넌트까지 리렌더링 되기 때문에 불필요한 처리(React.Memo() 등을 사용한 메모이제이션)가 필요하다. 이를 방지하기 위해 context 컴포넌트를 분리하고 children을 사용한다.
import { createContext, useState } from "react";
export const CounterContext = createContext<{
count: number;
setCount: React.Dispatch<React.SetStateAction<number>>;
}>({ count: 0, setCount: () => {} });
export const CounterContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{ count, setCount }}>
{children}
</CounterContext.Provider>
);
};
children
을 사용하여 값을 전달해주고 있다. 이렇게 코드를 작성해주면, context를 사용하지 않는 컴포넌트는 직접 메모이제이션 하지 않아도 리렌더링에서 제외된다.
점점 속도가 빨라지고 매일 3,4시간씩 통학하는것도 힘들지만 그래도 수업을 들어보니 제대로 배우는것 같아서 재밌는것 같다. 배우면 배울수록 내가 그동안 어떤 💩들을 만들어왔는지 깊게 반성하게 되는 것 같다. 내일은 useEffect와 zustand를 배울거라고 하셨다. 졸작 프로젝트 하던 중에 상태관리 때문에 심하게 스트레스 받고 인생이 힘들고 우울하고 암울하고 탈모 온 것 같고 마음이 너무 아팠었는데 그 🔥HOT🔥한 Zustand를 배울 수 있게 되어 너무 기대된다.
글 이쁘게 쓰고 싶어서 스타일 적용해봤더니 미리보기에서 잘보이길래 열심히 썼는데 출간하면 안보인다 너무 억울하다 진짜 세상 이럴수가 업음