avatar
Ganymedian

#3. more hooks & custom hooks

Jul 3
·
51 min read

#3. more hooks & custom hooks

  1. Work through for prev. project

  2. useRef

  3. useMemo, useCallback, useId

  4. useReducer

  5. useContext

  6. zustand

  7. Custom Hook #1. useLocalStorage

  8. Custom Hook #2. useAxiosFetch

  9. 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;

각 태스크별로 중요한 요점을 정리해 봅시다.

  1. re-render 로그:

    • useEffect 훅을 종속성 배열 없이 사용하면, 컴포넌트가 매번 렌더링될 때마다 트리거 됩니다.

  2. 마운트 로그:

    • 빈 배열을 종속성 배열로 주면, 컴포넌트가 마운트될 때 한 번만 트리거 됩니다.

  3. name과 age가 변경될 때 화면 반영:

    // 대상 변수를 state 로 생성해줍니다.
    const [name, setName] = useState("John");
    const [age, setAge] = useState(24);
    
    // 상태 state 에 변화가 발생하면, 컴포넌트가 리렌더링 된다고 배웠죠.
    
    • nameage 상태 변수를 생성하고 이들의 변화를 반영할 수 있도록 setNamesetAge 함수를 사용합니다.

  4. document.title에 name과 age 반영:

    // document.title 에 밸류를 할당해주는 JS 의 익숙한 코드를 사용할 수 있습니다.
    useEffect(() => {
      document.title = `${name} is ${age} years old`;
    }, [name, age]);
    
    • nameage가 변경될 때마다 document.title 이 업데이트 됩니다.

  5. 심화과제: 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 의 두가지 사용방법과 특성

  1. useRef 는 Dom 접근용 앵커인, ref 를 생성합니다.

  2. useRef 는 렌더링을 관통하는 밸류 저장소 역할을 합니다.

  3. 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를 사용하여 부모로부터 전달된 refinput 요소에 할당합니다

부모 컴포넌트

// /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;

ParentComponentuseRef를 사용하여 inputRef를 생성하고, 이를 MyInput 컴포넌트에 전달합니다. handleClick 함수는 input 요소의 값을 콘솔에 출력하고, 두 번째 버튼 클릭 시 input 요소의 값을 'Hello, World!'로 설정합니다.

forwardRef 는 React 의 고차 컴포넌트 HOC 로, 컴포넌트를 입력받아서 조작된 컴포넌트를 리턴하는 React 내장 HOC 중 하나입니다.

HOC 에 대해서는 밑에서 좀 더 자세히 다루겠습니다.

한줄 요약: Ref 를 전달할 때에는 forwardRef 를 사용해야 한다.

요약

  1. useRef는 ref를 생성해서 DOM 요소에 접근할 수 있게 해줍니다.

  2. useRef는 렌더링과 상관없이 값을 저장할 수 있습니다.

  3. 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;
  1. 상태 변수 선언: countinput 상태 변수를 useState로 선언합니다.

  2. 비용이 높은 계산 함수: expensiveCalculation 함수는 입력 숫자의 두 배를 반환하지만, 계산에 많은 시간을 소모하는 연산을 포함합니다.

  3. useMemo 사용: useMemocount가 변경될 때만 expensiveCalculation 함수를 호출하여 결과를 저장합니다. 이로써 input의 변화에 상관없이 count가 변경되지 않으면 재계산하지 않습니다.

  4. 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;
  1. 상태 변수 선언: count 상태 변수를 useState로 선언합니다.

  2. useCallback 사용: handleClick 함수를 useCallback으로 감싸 count가 변경될 때만 함수를 재생성하도록 합니다.

  3. 자식 컴포넌트: ChildComponentReact.memo로 감싸 불필요한 렌더링을 방지합니다. handleClick 함수가 변경되지 않으면 ChildComponent는 다시 렌더링되지 않습니다.

  4. React.memo는 고차 컴포넌트(HOC)로, props가 변경되지 않으면 컴포넌트를 다시 렌더링하지 않습니다. 즉, props가 동일하면 이전 렌더링 결과를 재사용합니다.

  5. 즉, 이 조합으로 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;
  1. useId 사용: useId 훅을 호출하여 고유한 식별자를 생성합니다.

  2. ID 사용: 생성된 식별자를 labelinput 요소의 htmlForid 속성에 사용하여 연결합니다. 이렇게 하면 서버 사이드 렌더링 에서도 고유한 ID 식별자를 확보할 수 있습니다.

.

.

useReducer

앞 장에서, React 의 모든 상태 stateuseState 로 생성되고, 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 를 사용하는 코드에서는,

  1. Action

  2. Reducer Function

  3. 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;

단계별 설명

  1. 초기 상태 정의: initialState 객체를 통해 초기 상태를 정의합니다.

  2. Action 타입 정의: INCREMENT, DECREMENT, SET_TEXT와 같은 액션 타입을 상수로 정의합니다.

  3. Reducer 함수 작성: reducer 함수는 현재 상태와 액션을 받아 새로운 상태를 반환합니다.

    • INCREMENT 액션: count 값을 1 증가시킵니다.

    • DECREMENT 액션: count 값을 1 감소시킵니다.

    • SET_TEXT 액션: text 값을 업데이트합니다.

  4. AACount 컴포넌트: countdispatch를 props로 받아서 렌더링하고, dispatch를 사용하여 count를 증가/감소시킵니다. React.memo를 사용하여 props가 변경되지 않으면 컴포넌트를 다시 렌더링하지 않습니다.

  5. AAText 컴포넌트: textdispatch를 props로 받아서 렌더링하고, dispatch를 사용하여 text 값을 업데이트합니다. React.memo를 사용하여 props가 변경되지 않으면 컴포넌트를 다시 렌더링하지 않습니다.

  6. useReducer 훅 사용: useReducer 훅을 사용하여 상태와 dispatch 함수를 생성합니다.

  7. 상태 업데이트: dispatch 함수를 사용하여 액션을 디스패치하고 상태를 업데이트합니다.

    • dispatch({ type: INCREMENT })count 값을 1 증가시킵니다.

    • dispatch({ type: DECREMENT })count 값을 1 감소시킵니다.

    • dispatch({ type: SET_TEXT, payload: e.target.value })text 값을 e.target.value 로 업데이트합니다.

  8. 렌더링: HOC React.memo 로 메모이제이션 되었으므로, 오직 업데이트된 상태.속성 을 참조하고 있는 컴포넌트만이 렌더링됩니다.

다시한번 정리하는 useReducer :

  1. Action 으로 Reducer 에서 사용할 Action 의 종류를 선언한다.

  2. Reducer 펑션을 작성한다. Reducer 펑션에서는 Action 으로 선언된 각각의 액션에 대해 처리할 코드들을 각 Action 에 대해 할당해준다.

  3. 상태 변경을 발생시키는 컴포넌트의 코드에서 dispatch( Action.Type ) 펑션을 바인딩 해준다.

  4. 상태가 변경되면서 컴포넌트의 리렌더링이 발생한다.

.

.

React 고차 컴포넌트 Higher-Order Components, HOC

고차 컴포넌트(Higher-Order Components, HOC) 란, React 에서 재사용 가능한 컴포넌트를 생성하는 패턴 중 하나입니다.

HOC 는, 쉽게 풀어서 설명하자면, 컴포넌트를 입력 받아서 새로운 조작된 컴포넌트를 반환하는 함수입니다.

이를 통해 공통 로직을 여러 컴포넌트 간에 쉽게 공유할 수 있습니다.

기억해 둘만한, React 의 HOC 로는, 오늘 배운, ForwardrefReact.memo 가 있습니다.

오늘 본문에서 다룬 ForwardrefReact.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의 주요 특징

  1. 간단한 API: Zustand는 간단하고 직관적인 API를 제공하여 복잡한 설정 없이도 상태 관리를 할 수 있습니다.

  2. Hooks 기반: React Hooks와 함께 사용되어 함수형 컴포넌트에서도 효율적으로 상태를 관리할 수 있습니다.

  3. 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;

설명

  1. create 함수: create 함수를 사용하여 Zustand 상태 스토어를 생성합니다. 이 함수는 상태를 업데이트하는 set 함수를 인자로 받습니다.

  2. useStore 함수: create 함수 내에서 setget을 매개변수로 받아서 사용합니다.

  3. 상태 스토어: useStore 함수를 호출하여 상태와 상태를 업데이트하는 메서드들을 가져옵니다.

  4. 컴포넌트에서 사용: useStore를 호출하면 상태와 업데이트 함수들을 훅 형태로 받아와서 컴포넌트에서 사용할 수 있습니다.

  5. input 의 키 입력에는 handleInputChange 펑션이 바인딩 되어 있고, handleInputChange 펑션에서는 setName(e.target.value) 가 실행됩니다.

  6. setName 펑션은, Zustand 의 create 에서 생성해준 메쏘드죠.

  7. setName 은, setName: (name) => set({ name }) 이렇게 실행하도록 훅이 만들어져 있습니다.

  8. setget 은, 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;

단계별 설명

  1. 초기 상태 설정:

    • useLocalStorage 훅은 keyinitialValue를 인자로 받습니다.

    • useState를 사용하여 로컬 스토리지에서 값을 가져오거나 초기값을 설정합니다.

    • window.localStorage.getItem(key)를 통해 로컬 스토리지에서 값을 가져오고, JSON.parse를 통해 객체로 변환합니다.

    • 만약 로컬 스토리지에 값이 없다면 initialValue를 반환합니다.

  2. 상태 업데이트 함수:

    • setValue 함수는 새로운 값을 설정하고, 로컬 스토리지에도 저장합니다.

    • 함수가 호출될 때 value가 함수라면, 현재 상태를 인자로 받아 새로운 값을 계산합니다.

    • window.localStorage.setItem(key, JSON.stringify(valueToStore))를 통해 값을 로컬 스토리지에 저장합니다.

  3. 훅 반환:

    • useLocalStorage 훅은 storedValuesetValue를 배열 형태로 반환합니다.

  4. 커스텀 훅 사용:

    • useLocalStorage 훅을 사용하여 로컬 스토리지의 값을 읽고 쓸 수 있습니다.

    • userName은 로컬 스토리지에서 읽어온 값이고, setUserName은 값을 업데이트하는 함수입니다.

    • input 요소에서 onChange 이벤트를 통해 userName을 업데이트하고, 이 값은 로컬 스토리지에 저장됩니다.

세줄 요약:

  1. state 와 setState 펑션을 리턴 받도록 설계되었다는 점이 중요합니다.

  2. state 는 커스텀 훅 내부에서 선언되었지만, setState 함수가 App 으로 리턴 되었으므로, App 에서 setState 함수로 state 값을 변경하고 읽어올 수 있습니다.

  3. 커스텀 훅은 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;

설명

  1. Axios 인스턴스 생성: axios.create 메소드를 사용하여 baseURL을 설정한 Axios 인스턴스를 생성합니다.

  2. useAxiosFetch 커스텀 훅: useAxiosFetch 커스텀 훅은 endpoint를 받아와서 해당 endpoint에 대한 요청을 Axios 인스턴스를 통해 보냅니다.

  3. 사용 예제: DataFetchingComponent에서는 /posts와 같은 endpoint를 전달하여 데이터를 가져옵니다.

  4. AbortController 사용: AbortController를 사용하여 요청을 취소할 수 있습니다. fetchData 함수 내에서 signal 객체를 요청에 포함시켜 axios 요청을 생성합니다.

  5. useEffect와 클린업: useEffect 훅에서 반환된 함수를 사용하여, 컴포넌트가 언마운트될 때 abortController.abort()를 호출하여 요청을 취소합니다. 이는 axios 요청이 완료되지 않은 경우에도 요청을 중지할 수 있습니다.

.

.

.

.

second project : upgrade useAxiosFetch

useAxiosFetch 커스텀 훅은, 외부 API 와의 Async 통신에 필요한 대부분의 상태와 결과를 리턴하고 있습니다.

그러나, 우리의 useAxiosFetch 훅은, 아직까지는 단지 한가지… 읽어오기 밖에는 할 수가 없었죠.

만약에, CRUD 모두를 담당하는 커스텀 훅이 된다면, 매우 유용한 툴이 될 수 있을 겁니다.

Fetch 상태와 결과 데이터, 그리고 에러를 리턴하는 데에다가, abortController 까지 장착한.. 매우 완성도 높은 훅이 되겠죠.

창고에 보관해 뒀다가 실전 프로젝트에서 꺼내 쓴다면, 개발 생산성에서도 크게 도움이 되겠죠.

아마도, react-query 또는 swr 등의, 잘 알려진 fetch 라이브러리에 익숙해질 때 까지는 활용도가 높은 커스텀 훅이 될 수 있을 것입니다. 어쩌면, React 로 코딩하고 있는 동안에는 두고두고 꺼내 쓰게 될지도 모릅니다.

그러니, 잘 만들어서 창고에 고이 보관해 둡시다.

useAxiosFetch 커스텀 훅이,

  1. read

  2. write

  3. update

  4. 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 포트를 사용하나 다른 포트를 지정할 수도 있습니다.

이제

http://localhost:3001/

에서 각 endpoint 로 API 요청을 할 수 있습니다.

  1. Read (GET): 모든 포스트 읽기

    GET <http://localhost:3001/post>
    
  2. Read (GET): 특정 id의 포스트 읽기

    GET <http://localhost:3001/posts/1>
    
  3. Update (PUT): 특정 id의 포스트 업데이트

    PUT <http://localhost:3001/posts/1>
    
  4. 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.







....