#3. more hooks & custom hooks
#3. more hooks & custom hooks
Work through for prev. project
useRef
useMemo, useCallback, useId
useReducer
useContext
zustand
Custom Hook #1. useLocalStorage
Custom Hook #2. useAxiosFetch
second project : upgrade-useAxiosFetch
.
Work through for prev. project
App.jsx 설정
먼저 App.jsx
파일을 설정합니다. 이 파일은 Exercise1
컴포넌트를 불러와서 로드 해주는 역할만 담당합니다.
// App.jsx
import React from "react";
import Exercise1 from "./exercises/Exercise1";
function App() {
return (
<>
<h1>UseEffectExercise</h1>
<Exercise1 />
</>
);
}
export default App;
Exercise1.jsx 설정
이 파일은 ChildForExercise1
컴포넌트를 조건부로 렌더링합니다. 버튼을 클릭하면 ChildForExercise1
컴포넌트를 보여주거나 숨깁니다.
// src/exercises/Exercise1.jsx
import React, { useState } from "react";
import ChildForExercise1 from "./components/ChildForExercise1";
const Exercise1 = () => {
const [show, setShow] = useState(true);
const childComponent = show ? <ChildForExercise1 /> : null;
return (
<div>
<button onClick={() => setShow((currentShow) => !currentShow)}>
Show/Hide
</button>
{childComponent}
</div>
);
};
export default Exercise1;
ChildForExercise1.jsx 설정
이 파일이 주 작업 파일입니다.
// /src/exercises/components/ChildForExercise1.jsx
import React, { useState, useEffect } from "react";
function ChildForExercise1() {
const [name, setName] = useState("John");
const [age, setAge] = useState(24);
const [debouncedName, setDebouncedName] = useState(name);
// 1. re-render 될 때마다 console.log("rerendering Component") 실행
// dependency array 가 없으므로 모든 렌더링에서 트리거 됩니다.
useEffect(() => {
console.log("rerendering Component");
});
// 2. 마운트될 때 console.log("Hi at mounting..") 실행
// dependency array 가 빈 배열이므로, 마운트 시에만 트리거 됩니다.
useEffect(() => {
console.log("Hi at mounting..");
}, []);
// 4. name과 age가 변경될 때 document.title에 반영
// dependency 로, name 과 age 가 주어졌으므로,
// dependency 에 변화가 있을 때마다 트리거 됩니다.
useEffect(() => {
document.title = `${debouncedName} is ${age} years old`;
}, [debouncedName, age]);
// 3. name과 age가 변경될 때 화면에 반영
// 5. name 필드의 값이 변경된 직후라도, 1초의 인터벌을 두고 컴포넌트의 리-렌더링이 일어나도록 관리
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedName(name);
}, 1000);
return () => {
clearTimeout(handler);
};
}, [name]);
return (
<div>
<input
type="text"
name="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<br />
<br />
<button onClick={() => setAge((a) => a - 1)}>-</button>
{age}
<button onClick={() => setAge((a) => a + 1)}>+</button>
<br />
<br />
Your name is {debouncedName}, you're {age} years old. You'll never get old in my app.
</div>
);
}
export default ChildForExercise1;
각 태스크별로 중요한 요점을 정리해 봅시다.
re-render 로그:
useEffect
훅을 종속성 배열 없이 사용하면, 컴포넌트가 매번 렌더링될 때마다 트리거 됩니다.
마운트 로그:
빈 배열을 종속성 배열로 주면, 컴포넌트가 마운트될 때 한 번만 트리거 됩니다.
name과 age가 변경될 때 화면 반영:
// 대상 변수를 state 로 생성해줍니다. const [name, setName] = useState("John"); const [age, setAge] = useState(24); // 상태 state 에 변화가 발생하면, 컴포넌트가 리렌더링 된다고 배웠죠.
name
과age
상태 변수를 생성하고 이들의 변화를 반영할 수 있도록setName
과setAge
함수를 사용합니다.
document.title에 name과 age 반영:
// document.title 에 밸류를 할당해주는 JS 의 익숙한 코드를 사용할 수 있습니다. useEffect(() => { document.title = `${name} is ${age} years old`; }, [name, age]);
name
과age
가 변경될 때마다document.title
이 업데이트 됩니다.
심화과제: 1초의 인터벌을 두고 상태 변화 로 이어지도록 함
// useEffect(() => { const handler = setTimeout(() => { setDebouncedName(name); }, 1000); return () => { clearTimeout(handler); }; }, [name]);
키입력 이벤트가 발생하면, 직전의 타이머가 리셋되는 구조죠. 만약 0.8 초 간격으로 키 입력이 연속된다면, 최후에 입력된 키 이벤트에만 setTimeout() 이벤트가 바인딩 되고
문제 제출시 제공한 기본 코드가 좀 잘못되어서 controlled input 이 제공되었더라고요. 죄송합니다. 이 때문에 어거지로 debouncedName 이라는 state 를 땜빵으로 넣어줬습니다.
과제의 취지를 이해하고 그 솔루션이 되는 코드로 마무리 하는 것으로 이해해주시면 좋겠습니다.
여기까지가 지난 과제에 대한 workthrough 였습니다.
수고 하셨습니다.
.
.
#3. more hooks & custom hooks
useRef
useRef
는 React 에서 매우 특별한 활용성을 갖는 훅입니다.
useRef
의 두가지 사용방법과 특성
useRef
는 Dom 접근용 앵커인,ref
를 생성합니다.useRef
는 렌더링을 관통하는 밸류 저장소 역할을 합니다.ref
는 일반적인 프롭 방식으로는 자식 컴포넌트에게 전달할 수 없습니다.
useRef
의 ref 생성과 ref 가 사용되는 방법
useRef
를 사용하여 DOM 요소에 접근할 수 있습니다.
이를 통해 직접 DOM 요소에 접근하여 조작할 수 있습니다.
먼저 우리의 웹앱 애플리케이션을 수정해서 오늘 배울 장을 준비합시다.
// App.jsx
import InputValueLogger from "./study/lecture3/InputValueLogger"
// Hello World React & useRef
function App() {
return (
<>
<h1>Hello World React</h1>
<InputValueLogger />
</>
);
}
export default App;
다음…
// /src/study/lecture3/InputValueLogger.jsx
import { useRef } from 'react';
function InputValueLogger() {
const inputRef = useRef(null);
const handleClick = () => {
// input 요소의 값을 읽습니다.
if (inputRef.current) {
console.log(inputRef.current.value);
inputRef.current.focus();
}
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Log Input Value</button>
</div>
);
}
export default InputValueLogger;
이 예제에서는 useRef
를 사용하여 input
요소의 ref를 생성하고,
버튼 클릭 시 input
요소의 밸류를 읽어와 로그한 후 포커스를 설정합니다.
ref.current
ref.current
에는, ref
가 지정된 DOM 객체 자체가 할당됩니다. 따라서, input
객체에서 사용할 수 있는 모든 속성과 펑션을 사용할 수 있습니다.
useRef
가 렌더링을 관통하는 저장소로 사용되는 사례
App.jsx 부터 준비해 줍니다.
// App.jsx
import InputValueLogger from "./study/lecture3/InputValueLogger"
import Timer from "./study/lecture3/Timer"
// Hello World React & useRef
function App() {
return (
<>
<h1>Hello World React</h1>
<InputValueLogger />
<Timer />
</>
);
}
export default App;
다음…
useRef
를 사용하여 컴포넌트의 상태 값을, 렌더링을 초월? 해서 저장할 수 있습니다.
이는 컴포넌트가 다시 렌더링되어도 값이 유지되도록 할 때 유용합니다.
// /src/study/lecture3/Timer
import { useRef, useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
const interval = setInterval(() => {
countRef.current += 1;
setCount(countRef.current);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Count: {count}</div>;
}
export default Timer;
이 예제에서는 useRef
를 사용하여 count
값을 저장하고, setInterval
을 통해 매초마다 count
값을 증가시킵니다.
컴포넌트가 다시 렌더링되더라도 countRef.current
값은 유지됩니다.
ref
의 Prop 전달 방식
다른 컴포넌트에 ref
를 프롭으로 전달하려면 forwardRef
를 사용해야 합니다.
forwardRef
는 부모 컴포넌트가 자식 컴포넌트의 DOM 노드에 접근할 수 있도록 합니다.
아래의 예제에서는 forwardRef
를 사용하여 부모 컴포넌트에서 자식 컴포넌트의 input
요소에 접근하고, 값을 읽고 쓸 수 있도록 합니다.
App.jsx 부터 준비해 줍니다.
// App.jsx
import InputValueLogger from "./study/lecture3/InputValueLogger"
import Timer from "./study/lecture3/Timer"
import ParentComponent from "./study/lecture3/ParentComponent"
// Hello World React & useRef
function App() {
return (
<>
<h1>Hello World React</h1>
<InputValueLogger />
<Timer />
<ParentComponent />
</>
);
}
export default App;
다음…
자식 컴포넌트
// /src/study/lecture3/children/MyInput.jsx
// 부모 컴포넌트로부터 ref 를 전달 받아서 input DOM 요소에 할당해줍니다.
// 이제 부모 컴포넌트가 ref 로, input 요소의 값 등 속성에 접근할 수 있게 됩니다.
import React, { forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => (
<div>
<label>Enter Text: </label>
<input ref={ref} type="text" {...props} />
</div>
));
export default MyInput;
MyInput
컴포넌트는 forwardRef
를 사용하여 부모로부터 전달된 ref
를 input
요소에 할당합니다
부모 컴포넌트
// /src/study/lecture3/ParentComponent.jsx
// 부모 컴포넌트에서 자식 컴포넌트의 ref 요소에 접근하고 컨트롤 할 수 있게 됩니다.
import React, { useRef } from 'react';
import MyInput from './MyInput';
function ParentComponent() {
const inputRef = useRef(null);
const handleClick = () => {
if (inputRef.current) {
console.log(inputRef.current.value);
}
};
return (
<div>
<MyInput ref={inputRef} />
<button onClick={handleClick}>Log Input Value</button>
<button onClick={() => (inputRef.current.value = 'Hello, World!')}>Set Input Value</button>
</div>
);
}
export default ParentComponent;
ParentComponent
는 useRef
를 사용하여 inputRef
를 생성하고, 이를 MyInput
컴포넌트에 전달합니다. handleClick
함수는 input
요소의 값을 콘솔에 출력하고, 두 번째 버튼 클릭 시 input
요소의 값을 'Hello, World!'로 설정합니다.
forwardRef
는 React 의 고차 컴포넌트 HOC 로, 컴포넌트를 입력받아서 조작된 컴포넌트를 리턴하는 React 내장 HOC 중 하나입니다.
HOC 에 대해서는 밑에서 좀 더 자세히 다루겠습니다.
한줄 요약: Ref 를 전달할 때에는 forwardRef
를 사용해야 한다.
요약
useRef
는 ref를 생성해서 DOM 요소에 접근할 수 있게 해줍니다.useRef
는 렌더링과 상관없이 값을 저장할 수 있습니다.forwardRef
를 사용하여ref
를 프롭으로 전달해서 접근하고 컨트롤 할 수 있습니다.
React 19 에서 forwardRef 소멸 예정
forwardRef
는 다행히? React 19 에서 없어질 것이라는 소식이 있었죠.
// 이런 형태로 전달할 수 있게 된다고 합니다.
const Button = ({ ref, ...props }) => {
return <button ref={ref} {...props} />
}
정확한 소식인지는 아직 모르겠습니다만, 그런 이야기가 있는 걸 보면, 메타에서도 ref
의 프롭 전달 방식에 불편과 문제가 있어왔다는 인식은 갖고 있는 것 같습니다.
.
.
useMemo, useCallback, useId
useMemo
useMemo
는 성능 최적화를 위해 사용되는 훅으로, 계산 비용이 높은 연산의 결과를 메모이제이션(memoization)하여 필요할 때만 재계산하도록 합니다.
dependency 의 특정 값이 변경될 때만 함수의 결과를 다시 계산합니다.
// /src/study/lecture3/ExpensiveCalculationComponent.jsx
import React, { useState, useMemo } from 'react';
function ExpensiveCalculationComponent() {
const [count, setCount] = useState(0);
const [input, setInput] = useState('');
const expensiveCalculation = (num) => {
console.log('Calculating...');
for (let i = 0; i < 1000000; i++) {} // 시간이 많이 걸리는 연산
return num * 2;
};
const result = useMemo(() => expensiveCalculation(count), [count]);
return (
<div>
<h1>Expensive Calculation with useMemo</h1>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type something"
/>
<p>Input: {input}</p>
<p>Count: {count}</p>
<p>Result: {result}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
</div>
);
}
export default ExpensiveCalculationComponent;
상태 변수 선언:
count
와input
상태 변수를useState
로 선언합니다.비용이 높은 계산 함수:
expensiveCalculation
함수는 입력 숫자의 두 배를 반환하지만, 계산에 많은 시간을 소모하는 연산을 포함합니다.useMemo 사용:
useMemo
는count
가 변경될 때만expensiveCalculation
함수를 호출하여 결과를 저장합니다. 이로써input
의 변화에 상관없이count
가 변경되지 않으면 재계산하지 않습니다.UI 업데이트:
input
의 값을 변경해도 비용이 높은 계산 함수는 호출되지 않습니다.count
를 증가시킬 때만 계산 함수가 호출됩니다.
useCallback
useCallback
은 함수의 메모이제이션을 위한 훅으로, 특정 값이 변경될 때만 함수를 재생성합니다.
주로 자식 컴포넌트에 콜백 함수를 전달할 때 사용되어 불필요한 재생성을 방지합니다.
// /src/study/lecture3/UseCallbackComponent.jsx
import React, { useState, useCallback } from 'react';
const ChildComponent = React.memo(({ onClick }) => {
console.log('Child component rendered');
return <button onClick={onClick}>Click me</button>;
});
function UseCallbackExample() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<h1>useCallback Example</h1>
<p>Count: {count}</p>
<ChildComponent onClick={handleClick} />
</div>
);
}
export default UseCallbackExample;
상태 변수 선언:
count
상태 변수를useState
로 선언합니다.useCallback 사용:
handleClick
함수를useCallback
으로 감싸count
가 변경될 때만 함수를 재생성하도록 합니다.자식 컴포넌트:
ChildComponent
는React.memo
로 감싸 불필요한 렌더링을 방지합니다.handleClick
함수가 변경되지 않으면ChildComponent
는 다시 렌더링되지 않습니다.React.memo
는 고차 컴포넌트(HOC)로, props가 변경되지 않으면 컴포넌트를 다시 렌더링하지 않습니다. 즉, props가 동일하면 이전 렌더링 결과를 재사용합니다.즉, 이 조합으로
ParentComponent
가 리-렌더링되어도handleClick
함수의 참조가 변경되지 않으면ChildComponent
는 다시 렌더링되지 않습니다.
useId
useId
는 React 18에서 도입된 훅으로, 고유한 식별자를 생성합니다.
주로 접근성 및 서버 사이드 렌더링(SSR) 에서 중복되지 않는 ID를 생성하는 데 사용됩니다.
//
import React, { useId } from 'react';
function FormComponent() {
const id = useId();
return (
<div>
<h1>useId Example</h1>
<label htmlFor={`${id}-username`}>Username:</label>
<input id={`${id}-username`} type="text" />
<br />
<label htmlFor={`${id}-password`}>Password:</label>
<input id={`${id}-password`} type="password" />
</div>
);
}
export default FormComponent;
useId 사용:
useId
훅을 호출하여 고유한 식별자를 생성합니다.ID 사용: 생성된 식별자를
label
과input
요소의htmlFor
및id
속성에 사용하여 연결합니다. 이렇게 하면 서버 사이드 렌더링 에서도 고유한 ID 식별자를 확보할 수 있습니다.
.
.
useReducer
앞 장에서, React 의 모든 상태 state
는 useState
로 생성되고, useReducer
로는 복합상태
를 생성한다… 라고 잠깐 언급해 드렸습니다.
복합상태 란, 예를 들면 다음과 같은 객체가 state 로 선언되었을 때를 말합니다.
//
const initialState = {
count: 0,
text: "Hello",
numArray: [1,2,3,4,5],
dataObj: {
author: "Kim SeungOk",
city: "Moojin",
year: 1970,
}
};
initialState 처럼, 두가지 이상의 속성을 포함하는 state
에서, 단 한가지 속성값 변화가 발생하는 경우에도,
state
에 변화가 발생하였으므로, 컴포넌트의 리랜더링이 발생하게 됩니다.
그러나 만약에,
initialState 는 AAA 컴포넌트에서 생성하였고, AACount 컴포넌트와, AAText 컴포넌트, AAData 컴포넌트를 각각 자식 컴포넌트로 갖고 있으며, 각각의 자식 컴포넌트에게 count 와 text, dataObj 를 프롭으로 전달해주고 있는 상황이라고 가정해 봅시다.
Component AAA
Component AACount
Component AAText
Component AADataObj
사용자가 count 속성에 변화를 일으키는 액션을 발생시켰을 때, count 에만 변화가 발생하였음에도 불구하고, text 와 dataObj 속성을 참조하는 컴포넌트 역시 re-rendering 됩니다. count, text, dataObj 세가지 속성이 모두 하나의 state
에 포함되어 있기 때문이죠. state
에 변경이 발생하였으므로, AACount, AAText, AADataObj 세개의 컴포넌트 모두에서 re-rendering 이 발생 됩니다.
이 문제는 useReducer 로 해결할 수 있습니다.
// /src/study/lecture3/useReducer/AAA.jsx
// 정확히는, useReducer 만으로는 자식 컴포넌트들인 AACount, AAText 가 모두 리렌더링 되는 문제를 피할 수 없습니다.
// count 와 text 는 여전히 하나의 상태 안에 묶여있는 한몸이기 때문이지요.
// 하지만 useReducer 없이는 이 문제를 해결할 수 없다고 말하는 쪽이 더 정확할 것입니다.
// 아래는, AACount, AAText 의 리랜더링 문제를,
// useReducer 와 함깨, React 의 또다른 내장 HOC 펑션인, React.memo 를 사용해서 해결한 코드입니다.
// React.memo 는 컴포넌트를 메모이제이션하는 고차 컴포넌트(HOC)입니다.
// 즉, 메모이제이션된 컴포넌트는 props 에 변화가 없는 한, 리렌더링이 발생하지 않습니다.
// AACount 컴포넌트를 메모이제이션 합니다.
// 프롭인 count 와 dispatch 에 변화가 없는 한, 메모이제이션이 유지됩니다.
const AACount = React.memo(({ count, dispatch }) => {
console.log('AACount rendered');
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
});
// AAText 컴포넌트를 메모이제이션 합니다.
// 프롭인 text 와 dispatch 에 변화가 없는 한, 메모이제이션이 유지됩니다.
const AAText = React.memo(({ text, dispatch }) => {
console.log('AAText rendered');
return (
<div>
<p>Text: {text}</p>
<button onClick={() => dispatch({ type: 'setText', payload: 'New Text' })}>
Change Text
</button>
</div>
);
});
const AAA = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<AACount count={state.count} dispatch={dispatch} />
<AAText text={state.text} dispatch={dispatch} />
</div>
);
};
// 이것은, 이해를 돕기 위한 코드일 뿐, 이런 방식의 코드가 바람직하다는 가이드는 아닙니다.
useReducer
훅은, state 밸류 와 상태를 업데이트하는 펑션
인 dispatch
를 튜플로 리턴합니다.
// useReducer 의 사용 문법... 무조건 외워 두세요.
const [state, dispatch] = useReducer(reducer, initialState);
// dispatch 의 사용 문법
dispatch({ type: Action.Type , payload: value })
reducer 와 dispatch 이해하기
우리 일상생활에서의 사고 회로로 reducer 와 dispatch 곧바로 이해하기는 좀 어렵습니다.
먼저 외운 뒤, 그러고 나서 조금씩 이해하게 되는 것들이 있는데, reducer 와 dispatch 가 그 중 하나라고 봅니다.
useReducer 의 구문을 외우고, 조금씩 익숙해지다 보면 그 논리구조가 이해되는 날이 곧 올 거에요.
useReducer 를 사용하는 코드에서는,
Action
Reducer Function
Dispatch Function
이렇게 세가지 중요한 Steps 가 있습니다. 이것도 일단 외워 두세요.
각각의 Steps 에 해당하는 소스코드 예제를 먼저 볼까요.
Step 1: Action
먼저 Action을 정의합니다. Action은 복합상태의 분기 Key, 즉 상태 업데이트의 타입을 선언합니다.
// /src/study/lecture3/useReducer/Action/Action.js
// Action
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
const SET_TEXT = "SET_TEXT";
Step 2: Reducer Function
Reducer 함수는 현재 상태와 action을 받아 새로운 상태를 반환하는 함수입니다.
// /src/study/lecture3/useReducer/Reducer/reducer.js
// reducer
// 각각의 Action Type 에 대해 실행될 코드를 할당해줍니다.
function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
case SET_TEXT:
return { ...state, text: action.payload };
default:
return state;
}
}
// Action 의 Key 들을 선언해주지 않고,
// case "INCREMENT":
// 이렇게 사용해도 당장 문제가 되지는 않습니다만,
// action.type 선언을 해주면, 오타 에러를 미연에 방지할 수 있습니다.
// Type Driven 이라는 철학에도 맞고, 여러모로 상수를 선언하고 진행하는 게 좋습니다.
return { ...state, count: state.count + 1 };
// 이 구문은 ES6 객체 스프레드 문법 + count 속성 값의 업데이트를 의미합니다.
// ...state 는 state 의 모든 속성을 해체하고,
// count: 3, 는 count 속성에 3 을 할당하는 의미로,
// state 에서 count 속성 값만 3 으로 업데이트 해서 리턴한다는 의미입니다.
// 어렵지 않죠?
..
Step 3: Dispatch Function
이제 컴포넌트에서 dispatch
를 사용하여 상태를 업데이트할 수 있습니다.
AACount 컴포넌트
// /src/study/lecture3/useReducer/children/AACount.jsx
// AACount
import React from 'react';
const AACount = React.memo(({ count, dispatch }) => {
console.log('AACount rendered');
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: INCREMENT })}>Increment</button>
<button onClick={() => dispatch({ type: DECREMENT })}>Decrement</button>
</div>
);
});
export default AACount;
AAText 컴포넌트
// /src/study/lecture3/useReducer/children/AAText.jsx
// AAText
import React from 'react';
const AAText = React.memo(({ text, dispatch }) => {
console.log('AAText rendered');
return (
<div>
<input
type="text"
value={text}
onChange={(e) => dispatch({ type: SET_TEXT, payload: e.target.value })}
/>
<p>Text: {text}</p>
</div>
);
});
export default AAText;
App 컴포넌트
// App
import React, { useReducer } from 'react';
import AACount from './AACount';
import AAText from './AAText';
const initialState = {
count: 0,
text: "Hello",
};
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h1>useReducer Example with Memoized Components</h1>
<AACount count={state.count} dispatch={dispatch} />
<AAText text={state.text} dispatch={dispatch} />
</div>
);
}
export default App;
단계별 설명
초기 상태 정의:
initialState
객체를 통해 초기 상태를 정의합니다.Action 타입 정의:
INCREMENT
,DECREMENT
,SET_TEXT
와 같은 액션 타입을 상수로 정의합니다.Reducer 함수 작성:
reducer
함수는 현재 상태와 액션을 받아 새로운 상태를 반환합니다.INCREMENT
액션:count
값을 1 증가시킵니다.DECREMENT
액션:count
값을 1 감소시킵니다.SET_TEXT
액션:text
값을 업데이트합니다.
AACount 컴포넌트:
count
와dispatch
를 props로 받아서 렌더링하고,dispatch
를 사용하여count
를 증가/감소시킵니다.React.memo
를 사용하여 props가 변경되지 않으면 컴포넌트를 다시 렌더링하지 않습니다.AAText 컴포넌트:
text
와dispatch
를 props로 받아서 렌더링하고,dispatch
를 사용하여text
값을 업데이트합니다.React.memo
를 사용하여 props가 변경되지 않으면 컴포넌트를 다시 렌더링하지 않습니다.useReducer 훅 사용:
useReducer
훅을 사용하여 상태와dispatch
함수를 생성합니다.상태 업데이트:
dispatch
함수를 사용하여 액션을 디스패치하고 상태를 업데이트합니다.dispatch({ type: INCREMENT })
는count
값을 1 증가시킵니다.dispatch({ type: DECREMENT })
는count
값을 1 감소시킵니다.dispatch({ type: SET_TEXT, payload: e.target.value })
는text
값을e.target.value
로 업데이트합니다.
렌더링: HOC React.memo 로 메모이제이션 되었으므로, 오직 업데이트된 상태.속성 을 참조하고 있는 컴포넌트만이 렌더링됩니다.
다시한번 정리하는 useReducer :
Action 으로 Reducer 에서 사용할 Action 의 종류를 선언한다.
Reducer 펑션을 작성한다. Reducer 펑션에서는 Action 으로 선언된 각각의 액션에 대해 처리할 코드들을 각 Action 에 대해 할당해준다.
상태 변경을 발생시키는 컴포넌트의 코드에서 dispatch( Action.Type ) 펑션을 바인딩 해준다.
상태가 변경되면서 컴포넌트의 리렌더링이 발생한다.
끗
.
.
React 고차 컴포넌트 Higher-Order Components, HOC
고차 컴포넌트(Higher-Order Components, HOC) 란, React 에서 재사용 가능한 컴포넌트를 생성하는 패턴 중 하나입니다.
HOC 는, 쉽게 풀어서 설명하자면, 컴포넌트를 입력 받아서 새로운 조작된 컴포넌트를 반환하는 함수입니다.
이를 통해 공통 로직을 여러 컴포넌트 간에 쉽게 공유할 수 있습니다.
기억해 둘만한, React 의 HOC 로는, 오늘 배운, Forwardref
와 React.memo
가 있습니다.
오늘 본문에서 다룬 Forwardref
와 React.memo
의 예제코드 에서, 두가지 모두 형식이 거의 똑같죠?
// memo
const MyComponent = React.memo((props) => {
// 컴포넌트 로직
});
// forwardRef
const MyComponent = React.forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
// 일반적인 ES6 의 펑션 선언문과 크게 다르지 않습니다.
Custom HOC
React 에서 누구나 커스텀 훅을 만들어서 사용할 수 있는 것 처럼, 누구나 커스텀 고차컴포넌트를 만들어서 사용할 수 있습니다.
// 인증용 HOC
import React, { useEffect } from 'react';
import { Redirect } from 'react-router-dom';
// HOC withAuth 정의
function withAuth(WrappedComponent) {
return function(props) {
const isAuthenticated = false; /* 인증 상태 체크 로직 */
if (!isAuthenticated) {
return <Redirect to="/login" />;
}
return <WrappedComponent {...props} />;
};
}
export default withAuth;
인증 HOC를 사용하여 컴포넌트를 감싸서 인증 성공/ 실패 각각에 리다이렉트 기능을 추가합니다.
// 커스텀 HOC 사용
import React from 'react';
import withAuthfrom './withAuth';
const MyComponent = withAuth((props) {
return <div>Welcome to the dashboard!</div>;
})
export default MyComponent
이제 MyComponent
는 사용자 인증 여부에 따라서 화면 출력 결과를 바꿉니다.
위 예제에서의 MyComponent
의 인증 작동 방식은 실전에서 사용되는 방식은 아닙니다.
그리고, HOC 는 자칫 ‘지나친 추상화’ 의 덫에 빠져서 코드의 복잡성을 심화시킬 수 있습니다. 꼭 필요한 곳에서만 사용하는 것이 좋습니다.
HOC 한줄요약: 그런게 있다는 정도만 기억해두자.
.
.
useContext
앞 선 장에서, Prop-Drilling 에 대해 잠깐 언급한 적이 있지요.
컴포넌트 구조의 Depth 가 깊어질 수록, 상태관리는 복잡해지게 되고, 또 전역적으로 사용되어야만 하는 state 들도 생기기 마련입니다.
이러한 상태관리 문제를 해결하기 위해서 React 에서 사용되는 React 의 내장 훅을 useContext
라고 합니다.
저의 다른 글, React Project : zustand-and-useContext
에서 context 에 관해서는 깊이있게 다루었기 때문에, 이번 강에서는, 살짝 그 원리만 짚고 넘어가는 것으로 마름하겠습니다. 보다 깊이 관심이 있는 분은
https://until.blog/@ganymedian/react-project---zustand-and-usecontext
의 글을 참고하시면 되겠습니다.
Context 의 사용 형식
import {createContext, useContext} from "react"
// 1. Context 생성
const SomeContext = createContext(undefined);
// 2. Context.Provider 생성return (
<SomeContext.Provider value={someValues}>
{children}
</SomeContext.Provider>
);
// 3. Context 소비// children 스코프 내에서 이렇게 가져다 사용하면 됩니다.
const someValues= useContext(SomeContext );
.
.
zustand
Zustand는 React 상태 관리 라이브러리로, 간단하면서도 강력한 상태 관리 솔루션을 제공합니다.
Zustand는 Context API 와 비슷하지만, 더욱 쉽고 편리하게 전역 상태를 관리할 수 있도록 돕습니다.
Zustand 설치
Zustand 는 React 의 외부, 3rd 파티 라이브러리입니다.
먼저 프로젝트에 설치해야 사용할 수 있어요.
// zustand install
# npm i zustand
Zustand의 주요 특징
간단한 API: Zustand는 간단하고 직관적인 API를 제공하여 복잡한 설정 없이도 상태 관리를 할 수 있습니다.
Hooks 기반: React Hooks와 함께 사용되어 함수형 컴포넌트에서도 효율적으로 상태를 관리할 수 있습니다.
DevTools 지원: 브라우저의 개발 도구를 통해 Zustand의 상태 변화를 디버깅할 수 있습니다.
Zustand 사용하기
Zustand를 사용하려면 create
함수를 사용하여 상태 스토어
를 생성하고, 이를 컴포넌트 내에서 훅 형태로 사용합니다.
// zustand 의 기본 구문
const useStore = create((set, get) => { .... });
Zustand 예제 코드
// /src/study/lecture3/zustand/Counter.jsx
// 프로젝트 어느 곳에서나 useStore 훅을 불러오면 전역 state 와 펑션들을 사용할 수 있게됩니다.
import create from 'zustand';
// zustand 용 커스텀 훅을 생성합니다.
const useStore = create((set, get) => ({
count: 0,
name: 'John Doe',
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
setName: (name) => set({ name }),
getName: () => get().name,
}));
// 컴포넌트에서 useStore 훅 사용
function Counter() {
const { count, name, increment, decrement, setName, getName } = useStore();
const handleInputChange = (event) => {
setName(event.target.value);
};
return (
<div>
<p>Name: {name}</p>
<input type="text" value={name} onChange={handleInputChange} />
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<p>Current Name (from getName): {getName()}</p>
</div>
);
}
export default Counter;
설명
create 함수:
create
함수를 사용하여 Zustand 상태 스토어를 생성합니다. 이 함수는 상태를 업데이트하는set
함수를 인자로 받습니다.useStore 함수:
create
함수 내에서set
과get
을 매개변수로 받아서 사용합니다.상태 스토어:
useStore
함수를 호출하여 상태와 상태를 업데이트하는 메서드들을 가져옵니다.컴포넌트에서 사용:
useStore
를 호출하면 상태와 업데이트 함수들을 훅 형태로 받아와서 컴포넌트에서 사용할 수 있습니다.input 의 키 입력에는
handleInputChange
펑션이 바인딩 되어 있고,handleInputChange
펑션에서는 setName(e.target.value) 가 실행됩니다.setName
펑션은, Zustand 의create
에서 생성해준 메쏘드죠.setName
은,setName: (name) => set({ name })
이렇게 실행하도록 훅이 만들어져 있습니다.set
과get
은,create
의 약속된 펑션입니다. 이름 그대로 set, get 역할을 담당하는 단순한 기능을 합니다.
Zustand의 장점
간편한 API: 초기 설정 없이 쉽게 사용할 수 있습니다.
성능 최적화: 불필요한 리렌더링을 방지하고 최적화된 상태 관리를 제공합니다.
Easy to learn: 배우고 적응하기가 매우 쉽습니다.
DevTools 지원: 브라우저의 개발 도구에서 상태 변화를 시각적으로 확인할 수 있습니다.
Zustand 에 관해서도
https://until.blog/@ganymedian/react-project---zustand-and-usecontext
의 글을 참고하시면 되겠습니다.
.
.
Custom Hook #1. useLocalStorage
로컬 스토리지에 데이터를 저장하고 꺼내오는 커스텀 훅을 만들어 봅시다.
매번 번거로운 코드로 로컬스토리지 코드를 작성하는 대신, 커스텀 훅을 사용하여 손쉽게 로컬 스토리지의 데이터를 핸들링할 수 있습니다.
커스텀 훅 useLocalStorage
// /src/study/lecture3/customHooks/useLocalStorage.jsx
// localStorage I/O 를 위한 커스텀 훅
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// window 객체가 없다면 localStorage 도 없다.
const isClient = typeof window !== 'undefined';
// 로컬 스토리지에서 값을 가져오거나 초기값을 설정.
const [storedValue, setStoredValue] = useState(() => {
if (!isClient) {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// 로컬 스토리지에 값을 저장하는 함수입니다.
const setValue = (value) => {
if (!isClient) {
console.warn('LocalStorage is not available');
return;
}
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
export default useLocalStorage;
// 이 커스텀 훅은 다양하게 활용될 수 있습니다
// [userName, setUserName] = useLocalStorage('user', 'John');
// [movieName, setMovieName] = useLocalStorage('movie', 'John Wick 4');
// ...etc
커스텀 훅 사용
// useLocalStorage 커스텀 훅 사용
import React from 'react';
import useLocalStorage from './useLocalStorage';
function App() {
const [userName, setUserName] = useLocalStorage('user', 'John');
return (
<div>
<h1>Hello, {userName}!</h1>
<input
type="text"
value={userName}
onChange={(e) => setUserName(e.target.value)}
/>
</div>
);
}
export default App;
단계별 설명
초기 상태 설정:
useLocalStorage
훅은key
와initialValue
를 인자로 받습니다.useState
를 사용하여 로컬 스토리지에서 값을 가져오거나 초기값을 설정합니다.window.localStorage.getItem(key)
를 통해 로컬 스토리지에서 값을 가져오고, JSON.parse를 통해 객체로 변환합니다.만약 로컬 스토리지에 값이 없다면
initialValue
를 반환합니다.
상태 업데이트 함수:
setValue
함수는 새로운 값을 설정하고, 로컬 스토리지에도 저장합니다.함수가 호출될 때
value
가 함수라면, 현재 상태를 인자로 받아 새로운 값을 계산합니다.window.localStorage.setItem(key, JSON.stringify(valueToStore))
를 통해 값을 로컬 스토리지에 저장합니다.
훅 반환:
useLocalStorage
훅은storedValue
와setValue
를 배열 형태로 반환합니다.
커스텀 훅 사용:
useLocalStorage
훅을 사용하여 로컬 스토리지의 값을 읽고 쓸 수 있습니다.userName
은 로컬 스토리지에서 읽어온 값이고,setUserName
은 값을 업데이트하는 함수입니다.input
요소에서onChange
이벤트를 통해userName
을 업데이트하고, 이 값은 로컬 스토리지에 저장됩니다.
세줄 요약:
state 와 setState 펑션을 리턴 받도록 설계되었다는 점이 중요합니다.
state 는 커스텀 훅 내부에서 선언되었지만, setState 함수가 App 으로 리턴 되었으므로, App 에서 setState 함수로 state 값을 변경하고 읽어올 수 있습니다.
커스텀 훅은 state와 setState 함수뿐만 아니라 여러 값과 펑션들을 리턴할 수 있습니다.
끗
.
.
Custom Hook #2. useAxiosFetch
자, 이제 매운 맛 커스텀 훅입니다. 추상화 기법이 들어가기 시작합니다.
먼저 Axios 를 설치합니다.
Axios 는 Javascript 내장 fetch 객체보다 편리한 async.fetch api 를 제공합니다.
// install axios
# npm i axios
커스텀 훅 useAxiosFetch
axios 에서는 바닐라 자바스크립트의 AbortController ****를 대체할 수 있는,
axios.CancelToken
을 제공합니다.
여기서는 axios.CancelToken
을 사용하겠습니다.
// /src/study/lecture3/customHooks/useAxiosFetch.jsx
// useAxiosFetch
import axios from 'axios';
// Axios 인스턴스 생성 및 설정
const axiosInstance = axios.create({
baseURL: '<https://api.example.com>', // BE baseUrl을 여기에 설정합니다.
});
// useAxiosFetch 커스텀 훅 정의
const useAxiosFetch = (endpoint) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const source = axios.CancelToken.source();
const fetchData = async () => {
try {
//const response = await axiosInstance.get(endpoint, { signal });
const response = await axiosInstance.request({
url: endpoint,
cancelToken: source.token,
});
setData(response.data);
setLoading(false);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Request aborted');
} else {
setError(err);
setLoading(false);
}
}
};
fetchData();
// 컴포넌트 언마운트 시 요청 취소
return () => {
//abortController.abort();
source.cancel('Operation canceled by user.');
};
}, [endpoint]);
return { data, loading, error };
};
export default useAxiosFetch;
useAxiosFetch 사용 예제
이제 useAxiosFetch
커스텀 훅을 사용하는 컴포넌트에서는 endpoint만을 전달하여 요청을 보낼 수 있습니다.
//
import React from 'react';
import useAxiosFetch from './useAxiosFetch';
const DataFetchingComponent = () => {
const { data, loading, error } = useAxiosFetch('/posts'); // endpoint를 전달합니다.
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>Posts</h1>
<ul>
{data.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
};
export default DataFetchingComponent;
설명
Axios 인스턴스 생성:
axios.create
메소드를 사용하여 baseURL을 설정한 Axios 인스턴스를 생성합니다.useAxiosFetch 커스텀 훅:
useAxiosFetch
커스텀 훅은 endpoint를 받아와서 해당 endpoint에 대한 요청을 Axios 인스턴스를 통해 보냅니다.사용 예제:
DataFetchingComponent
에서는/posts
와 같은 endpoint를 전달하여 데이터를 가져옵니다.AbortController 사용:
AbortController
를 사용하여 요청을 취소할 수 있습니다.fetchData
함수 내에서signal
객체를 요청에 포함시켜 axios 요청을 생성합니다.useEffect와 클린업:
useEffect
훅에서 반환된 함수를 사용하여, 컴포넌트가 언마운트될 때abortController.abort()
를 호출하여 요청을 취소합니다. 이는 axios 요청이 완료되지 않은 경우에도 요청을 중지할 수 있습니다.
.
.
.
.
second project : upgrade useAxiosFetch
useAxiosFetch 커스텀 훅은, 외부 API 와의 Async 통신에 필요한 대부분의 상태와 결과를 리턴하고 있습니다.
그러나, 우리의 useAxiosFetch 훅은, 아직까지는 단지 한가지… 읽어오기 밖에는 할 수가 없었죠.
만약에, CRUD 모두를 담당하는 커스텀 훅이 된다면, 매우 유용한 툴이 될 수 있을 겁니다.
Fetch 상태와 결과 데이터, 그리고 에러를 리턴하는 데에다가, abortController 까지 장착한.. 매우 완성도 높은 훅이 되겠죠.
창고에 보관해 뒀다가 실전 프로젝트에서 꺼내 쓴다면, 개발 생산성에서도 크게 도움이 되겠죠.
아마도, react-query 또는 swr 등의, 잘 알려진 fetch 라이브러리에 익숙해질 때 까지는 활용도가 높은 커스텀 훅이 될 수 있을 것입니다. 어쩌면, React 로 코딩하고 있는 동안에는 두고두고 꺼내 쓰게 될지도 모릅니다.
그러니, 잘 만들어서 창고에 고이 보관해 둡시다.
useAxiosFetch 커스텀 훅이,
read
write
update
delete
이렇게 네가지 endpoint 에 대해 모두 작동하도록 버전업 을 해봅시다.
Json Server 준비
프로젝트에서 자유롭게 읽고 쓰기를 할 수 있는 API endpoint 가 필요합니다.
jsonServer 를 구동 시켜서 프로젝트에서 사용할 endpoint 들을 준비합시다.
jsonServer 구동
JSON Server 설치
다음 명령어를 사용하여 JSON Server를 전역으로 설치합니다.
# npm install -g json-server
JSON 파일 생성
JSON Server는 JSON 파일을 데이터 소스로 사용합니다.
예를 들어,
db.json
파일을 프로젝트 폴더의 /api/ 에 생성하고 아래와 같은 내용을 추가합니다.// 폴더 구조는 이렇게 됩니다. // /src/App.jsx // /api/db.json
{ "posts": [ { "id": 1, "title": "JSON Server", "author": "John Doe" }, { "id": 2, "title": "REST API", "author": "Jane Smith" } ], "comments": [ { "id": 1, "body": "Some comment", "postId": 1 }, { "id": 2, "body": "Another comment", "postId": 1 } ] }
JSON Server 실행
다음 명령어로 JSON Server를 실행합니다. 이때,
db.json
파일의 경로를 지정합니다.# json-server --watch db.json --port 3001
-watch db.json
: JSON 파일의 변경을 감지합니다.-port 3001
: 서버를 3001 포트에서 실행합니다. 기본적으로 3000 포트를 사용하나 다른 포트를 지정할 수도 있습니다.
이제
에서 각 endpoint 로 API 요청을 할 수 있습니다.
Read (GET): 모든 포스트 읽기
GET <http://localhost:3001/post>
Read (GET): 특정 id의 포스트 읽기
GET <http://localhost:3001/posts/1>
Update (PUT): 특정 id의 포스트 업데이트
PUT <http://localhost:3001/posts/1>
Delete (DELETE): 특정 id의 포스트 삭제
DELETE <http://localhost:3001/posts/1>
Change baseURL
먼저 baseURL 을 json-server 로 바꿔줍니다.
이왕이면 폼 나게 환경변수로 관리해 줍시다.
.env 파일 설정
먼저 프로젝트 루트에 .env
파일을 생성하고, 다음과 같이 환경 변수를 정의합니다:
vite 프로젝트에서는 환경변수 네이밍에 규칙이 있습니다. VITE_ 를 접두어로 붙여줘야 합니다.
VITE_API_BASE_URL=https://localhost:3001
Axios 인스턴스 유틸리티 파일
그 다음, Axios 인스턴스를 전담하는 /src/util/axiosInstance.js 파일을 생성합니다. 이제 모든 axios 통신은 이 전담 유틸리티를 통해서만 이루어집니다.
// /src/util/axiosInstance.js
import axios from 'axios';
// Axios 인스턴스 생성 및 설정
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
});
export default axiosInstance;
사용 예시
//
import axiosInstance from './util/axiosInstance';
async function fetchData() {
try {
const response = await axiosInstance.get('/endpoint');
console.log(response.data);
} catch (error) {
console.error('Error fetching data:', error);
}
}
fetchData();
이제 axios 의 요청 코드에도 method 가 추가됩니다.
WRITE
// WRITE 요청 axios
const method = "POST"
const url = "/posts"
const data = { name: 'John' }
const headers = { 'Content-Type': 'application/json' }
const response = await axiosInstance.request({
method,
url,
data,
headers,
cancelToken: source.token,
});
..
UPDATE
// UPDATE 요청 axios
const method = "PUT"
const url = "/posts/1"
const data = { name: 'John' }
const headers = { 'Content-Type': 'application/json' }
const response = await axiosInstance.request({
method,
url,
data,
headers,
cancelToken: source.token,
});
..
DELETE
// DELETE 요청 axios
const method = "**DELETE**"
const url = "/posts/1"
//const data = { name: 'John' }
const headers = { 'Content-Type': 'application/json' }
const response = await axiosInstance.request({
method,
url,
headers,
cancelToken: source.token,
});
..
GET
// DELETE 요청 axios
const method = "GET"
const url = "/posts"
const params = { userId: 1 }
const headers = { 'Content-Type': 'application/json' }
const response = await axiosInstance.request({
method,
url,
params,
headers,
cancelToken: source.token,
});
..
axios 에서 제공하는 axiosInstance.get
, axiosInstance.post
, axiosInstance.delete
등을 사용할 수도 있습니다.
하지만, axiosInstance.request
메서드를 사용하면 하나의 코드로 4가지 요청을 모두 처리할 수 있다는 장점이 있죠. 각자가 원하는 대로 구성할 수 있을 것입니다.
.
.
마치면서…
이번 장은 분량이 좀 길어졌습니다.
아마도 오늘 이 장 까지 충실히 따라오셨다면, 코어 리액트의 80% 는 이해하셨다고 봐도 무방하다고 봅니다.
이제 남은 문제는, 얼마나 잘 숙달되고 활용하느냐… 의 문제이겠죠.
“기술은 배우는 게 아니라 숙달되는 것” 이라는 말을 저는 좋아합니다.
사실 리액트를 배우는 여정에서 코어 리액트는 빙산의 일각에 불과합니다.
오늘만 해도 외부 라이브러리인 zustand 와 axios custom hook 을 배웠죠.
다음 강 부터는 본격적으로 리액트 생태계에서 가장 자주 사용되는 라이브러리들을 다루게 됩니다.
Workthrough 와 함께 1~2 주 후에 뵙겠습니다.
긴 글 따라오시느라 수고하셨습니다.
fin.