[LG CNS AM CAMP 1기] 프론트엔드 8 | React

letthem·2025년 1월 6일
0

LG CNS AM CAMP 1기

목록 보기
8/16
post-thumbnail

성능 향상을 위한 Hook

useReducer

개념

  • 상태 관리 로직이 복잡할 때 useState 대체품으로 사용
  • 현재 상태와 액션(상태 변경에 필요한 정보)을 받아 새로운 상태를 반환하는 리듀서(reducer) 함수를 통해 상태를 관리
  • useReducer는 리듀스 함수와 초기 상태를 인자로 받아 상태와 디스패치 함수를 반환
  • 리듀스 함수 : 현재 상태와 액션을 받아 새로운 상태를 반환하는 순수 함수
  • 초기 상태 : 상태의 초기값
  • 액션 : 상태를 변경하기 위한 정보가 담긴 객체로, 일반적으로 type(어떤 상태를 나타내는 속성) 프로퍼티를 가지고 있으며, 필요에 따라 데이터를 포함할 수 있음
  • 디스패치 함수 : 액션을 리듀서로 전달해 상태를 업데이트하는 함수

사용 예 ⬇️

  1. 상태 로직이 복잡하거나, 여러 하위 값으로 구성된 상태를 관리해야 할 때
  2. 상태 업데이트 로직이 여러 종류의 액션에 의해 다르게 동작해야 할 때
  3. 상태와 그 업데이트 로직을 컴포넌트에서 분리하고 싶을 때

예제 ! ⬇️

useState를 이용해서 Counter 컴포넌트를 구현

  • e.target.innerText 이용하면 콘텐츠를 가져올 수 있다 !!
import { useReducer, useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  const changeCount = e => setCount(count + Number(e.target.innerText)) 
      <== 버튼의 내용(contents)이 숫자로 변환 가능하기 때문에 하나의 핸들러 함수로 구현이 가능
  return (
    <>
      <div>현재 카운터 값은 <b>{count}</b>입니다.</div>
      <div>
        <button onClick={changeCount}>+1</button>
        <button onClick={changeCount}>-1</button>
      </div>
    </>
  )
}

export default function App() {
  return (
    <>
      <Counter/>
    </>
  );
}

버튼의 내용이 숫자로 변환할 수 없는 경우 = 문자열인 경우 => 핸들러 함수를 따로 작성해야 함

import { useReducer, useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const changeCountPlus = () => setCount(count + 1);
  const changeCountMinus = () => setCount(count - 1);
  return (
    <>
      <div>
        현재 카운터 값은 <b>{count}</b>입니다.
      </div>
      <div>
        <button onClick={changeCountPlus}>하나 더하기</button>
        <button onClick={changeCountMinus}>하나 빼기</button>
      </div>
    </>
  );
};

export default function App() {
  return (
    <>
      <Counter />
    </>
  );
}

상태변수 변경 로직이 다양해져 버리면(ex. 곱하기, 나누기 등등 늘어날수록) 함수를 계속 만들어야 하는데 좋은 방법이 없을까? (사용 예 - 2)

useReducer를 이용해서 Counter 컴포넌트를 작성

import { useReducer, useState } from 'react';

// ⬇️ 리듀서 함수: 현재 상태를 어떻게 바꿀건지 !
// - state: 현재 상태 변수의 값
// - action: 상태 변수 변경에 필요한 조건과 값 (호출 시 전달되는 값)
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

const Counter = () => {
  /*
  const [count, setCount] = useState(0);

  const changeCountPlus = () => setCount(count + 1);
  const changeCountMinus = () => setCount(count - 1);
  */
  const [state, dispatch] = useReducer(reducer, { count: 0 }); // 리듀서 함수와 상태 변수 정의
  /*
  풀어서 쓰면 ⬇️
  state = { count: 0 },
  dispatch = (action) => {
    ..
    newState = reducer(state, action)
    state = { ...newState }
    Counter(); // 자기 자신 다시 호출 - rerendering...
    ..
  }
  */

  return (
    <>
      <div>
        현재 카운터 값은 <b>{state.count}</b>입니다.
      </div>
      <div>
        <button onClick={() => dispatch({ type: 'INCREMENT' })}>하나 더하기</button>
        <button onClick={() => dispatch({ type: 'DECREMENT' })}>하나 빼기</button>
      </div>
    </>
  );
};

export default function App() {
  return (
    <>
      <Counter />
    </>
  );
}

훨씬 간단해졌다 !!!

😇 장점

  • 상태 변수 정의하는 부분과 상태 변수 변경하는 로직이 분리되었다.
  • 이것을 별도의 함수로 분리할 수 있다!!!!! (사용 예 - 3)

reducer.js 로 분리해서 import 해서 쓸 수 있다 !! WoW

export default function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

(사용 예 - 1)
로직 만드는 사람과 적용하는 사람이 분리되서 쉽게 작업할 수 있다.
곱하기, 나누기 추가

import { useReducer, useState } from 'react';

// ⬇️ 리듀서 함수: 현재 상태를 어떻게 바꿀건지 !
// - state: 현재 상태 변수의 값
// - action: 상태 변수 변경에 필요한 조건과 값 (호출 시 전달되는 값)
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'TWOTIMES':
      return { count: state.count * 2 };
    case 'TWODIVIDE':
      return { count: state.count / 2 };
    default:
      return state;
  }
}

const Counter = () => {
  /*
  const [count, setCount] = useState(0);

  const changeCountPlus = () => setCount(count + 1);
  const changeCountMinus = () => setCount(count - 1);
  */
  const [state, dispatch] = useReducer(reducer, { count: 0 }); // 리듀서 함수와 상태 변수 정의
  /*
  풀어서 쓰면 ⬇️
  state = { count: 0 },
  dispatch = (action) => {
    ..
    newState = reducer(state, action)
    state = { ...newState }
    Counter(); // 자기 자신 다시 호출 - rerendering...
    ..
  }
  */

  return (
    <>
      <div>
        현재 카운터 값은 <b>{state.count}</b>입니다.
      </div>
      <div>
        <button onClick={() => dispatch({ type: 'INCREMENT' })}>하나 더하기</button>
        <button onClick={() => dispatch({ type: 'DECREMENT' })}>하나 빼기</button>
        <button onClick={() => dispatch({ type: 'TWOTIMES' })}>둘 곱하기</button>
        <button onClick={() => dispatch({ type: 'TWODIVIDE' })}>둘 나누기</button>
      </div>
    </>
  );
};

export default function App() {
  return (
    <>
      <Counter />
    </>
  );
}

이렇게도 가능하다 ⬇️

import { useReducer, useState } from 'react';

// ⬇️ 리듀서 함수: 현재 상태를 어떻게 바꿀건지 !
// - state: 현재 상태 변수의 값
// - action: 상태 변수 변경에 필요한 조건과 값 (호출 시 전달되는 값)
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'TWOTIMES':
      return { count: state.count * 2 };
    case 'TWODIVIDE':
      return { count: state.count / 2 };
    case 'TIMES':
      return { count: state.count * action.value };
    default:
      return state;
  }
}

const Counter = () => {
  /*
  const [count, setCount] = useState(0);

  const changeCountPlus = () => setCount(count + 1);
  const changeCountMinus = () => setCount(count - 1);
  */
  const [state, dispatch] = useReducer(reducer, { count: 0 }); // 리듀서 함수와 상태 변수 정의
  /*
  풀어서 쓰면 ⬇️
  state = { count: 0 },
  dispatch = (action) => {
    ..
    newState = reducer(state, action)
    state = { ...newState }
    Counter(); // 자기 자신 다시 호출 - rerendering...
    ..
  }
  */

  return (
    <>
      <div>
        현재 카운터 값은 <b>{state.count}</b>입니다.
      </div>
      <div>
        <button onClick={() => dispatch({ type: 'INCREMENT' })}>하나 더하기</button>
        <button onClick={() => dispatch({ type: 'DECREMENT' })}>하나 빼기</button>
        <button onClick={() => dispatch({ type: 'TWOTIMES' })}>둘 곱하기</button>
        <button onClick={() => dispatch({ type: 'TWODIVIDE' })}>둘 나누기</button>
        <button onClick={() => dispatch({ type: 'TIMES', value: 20 })}>20배수</button>
        <button onClick={() => dispatch({ type: 'TIMES', value: 30 })}>30배수</button>
      </div>
    </>
  );
};

export default function App() {
  return (
    <>
      <Counter />
    </>
  );
}

useState를 사용해서 여러 입력창의 상태를 관리

import { useState } from 'react';

const Info = () => {
  const [name, setName] = useState('');
  const [nickName, setNickName] = useState('');

  const changeName = e => setName(e.target.value);
  const changeNickName = e => setNickName(e.target.value);
  
  return (
    <>
      <div>
        <p>이름: {name}</p>
        <p>별명: {nickName}</p>
      </div>
      <div>
        <p>이름: <input type="text" value={name} onChange={changeName} /></p>
        <p>별명: <input type="text" value={nickName} onChange={changeNickName} /></p>
      </div>
    </>
  );
};

export default function App() {
  return (
    <>
      <Info />
    </>
  );
}

useReducer를 사용하는 것으로 변경

import { useReducer, useState } from 'react';

// action = { type: 변경할 상태변수, value: 변경할 값 }
// dispatch() 함수를 호출할 때 action 값을 설정해서 전달
const reducer = (state, action) => {
  switch (action.type) {
    case 'name':
      return { ...state, name: action.value };
    case 'nickName':
      return { ...state, nickName: action.value };
    default:
      return state;
  }
};

const Info = () => {
  /*
  const [name, setName] = useState('');
  const [nickName, setNickName] = useState('');

  const changeName = e => setName(e.target.value);
  const changeNickName = e => setNickName(e.target.value);
  */

  const [state, dispatch] = useReducer(reducer, { name: '', nickName: '' });
  return (
    <>
      <div>
        <p>이름: {state.name}</p>
        <p>별명: {state.nickName}</p>
      </div>
      <div>
        <p>
          이름:{' '}
          <input
            type="text"
            value={state.name}
            onChange={(e) => dispatch({ type: 'name', value: e.target.value })}
          />
        </p>
        <p>
          별명:{' '}
          <input
            type="text"
            value={state.nickName}
            onChange={(e) => dispatch({ type: 'nickName', value: e.target.value })}
          />
        </p>
      </div>
    </>
  );
};

export default function App() {
  return (
    <>
      <Info />
    </>
  );
}

reducer 함수 만들어주고, useReducer 정의해서 dispatch 함수를 호출하도록 만들기 !

상태 변수를 객체 비구조화해서 표현

state. 을 줄여보자 !

import { useReducer, useState } from 'react';

// action = { type: 변경할 상태변수, value: 변경할 값 }
// dispatch() 함수를 호출할 때 action 값을 설정해서 전달
const reducer = (state, action) => {
  switch (action.type) {
    case 'name':
      return { ...state, name: action.value };
    case 'nickName':
      return { ...state, nickName: action.value };
    default:
      return state;
  }
};

const Info = () => {
  /*
  const [name, setName] = useState('');
  const [nickName, setNickName] = useState('');

  const changeName = e => setName(e.target.value);
  const changeNickName = e => setNickName(e.target.value);
  */

  const [state, dispatch] = useReducer(reducer, { name: '', nickName: '' });
  const { name, nickName } = state;

  return (
    <>
      <div>
        <p>이름: {name}</p>
        <p>별명: {nickName}</p>
      </div>
      <div>
        <p>
          이름:{' '}
          <input
            type="text"
            value={name}
            onChange={(e) => dispatch({ type: 'name', value: e.target.value })}
          />
        </p>
        <p>
          별명:{' '}
          <input
            type="text"
            value={nickName}
            onChange={(e) => dispatch({ type: 'nickName', value: e.target.value })}
          />
        </p>
      </div>
    </>
  );
};

export default function App() {
  return (
    <>
      <Info />
    </>
  );
}

입력창에 name 속성을 추가해서 리듀서 함수와 이벤트 핸들러 함수를 단순화

import { useReducer } from 'react';

// action = { type: 변경할 상태변수, value: 변경할 값 }
// dispatch() 함수를 호출할 때 action 값을 설정해서 전달
const reducer = (state, action) => {
  /*
  switch (action.type) {
    case 'name':
      return { ...state, name: action.value };
    case 'nickName':
      return { ...state, nickName: action.value };
    default:
      return state;
  }
  */

  return { ...state, [action.type]: action.value }; // 계산된 속성명. action.type은 name 아니면 nickname
};

const Info = () => {
  /*
  const [name, setName] = useState('');
  const [nickName, setNickName] = useState('');

  const changeName = e => setName(e.target.value);
  const changeNickName = e => setNickName(e.target.value);
  */

  const [state, dispatch] = useReducer(reducer, { name: '', nickName: '' });
  const { name, nickName } = state;
  const changeValue = (e) => dispatch({ type: e.target.name, value: e.target.value });

  return (
    <>
      <div>
        <p>이름: {name}</p>
        <p>별명: {nickName}</p>
      </div>
      <div>
        <p>
          이름: <input type="text" name="name" value={name} onChange={changeValue} />
        </p>
        <p>
          별명: <input type="text" name="nickName" value={nickName} onChange={changeValue} />
        </p>
      </div>
    </>
  );
};

export default function App() {
  return (
    <>
      <Info />
    </>
  );
}

최종 코드 ⬇️

import { useReducer } from 'react';

const reducer = (state, action) => {
  return { ...state, [action.type]: action.value }; // 계산된 속성명. action.type은 name 아니면 nickname
};

const Info = () => {
  const [state, dispatch] = useReducer(reducer, { name: '', nickName: '' });
  const { name, nickName } = state;
  const changeValue = (e) => dispatch({ type: e.target.name, value: e.target.value });

  return (
    <>
      <div>
        <p>이름: {name}</p>
        <p>별명: {nickName}</p>
      </div>
      <div>
        <p>
          이름: <input type="text" name="name" value={name} onChange={changeValue} />
        </p>
        <p>
          별명: <input type="text" name="nickName" value={nickName} onChange={changeValue} />
        </p>
      </div>
    </>
  );
};

export default function App() {
  return (
    <>
      <Info />
    </>
  );
}

굉장히 간소화되었다 -!


useMemo

개념

성능 최적화를 위해 특정 값이 변경될 때만 메모이제이션된 값을 재계산하도록 하여 불필요한 계산을 방지

  • 메모이제이션: 미리 갖고 있던 값 (캐싱과 비슷하다)
    계산 비용이 높은 작업이나 렌더링 중에 자주 호출되는 작업에 유용하다 !
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  ~~~~~~
                              계산을 수행할 함수                     의존성 배열

useMemo로 전달된 함수는 렌더링이 일어나는 동안 실행 => 일반적으로 렌더링이 일어나는 동안 실행해서는 안 될 작업을 useMemo() 함수에 넣으면 안 됨 !!!
ex) 외부로부터 데이터 가져오는 것 - 렌더링이 제대로 안 된다

의존성 배열을 넣지 않을 경우, 렌더링이 일어날 때마다 매번 함수를 실행


예제 ⬇️

Average 컴포넌트에 저장된 숫자들의 평균을 구해서 출력하는 기능을 추가

import React, { useRef, useState } from 'react';

export default function Average() {
  const [number, setNumber] = useState('');
  const [list, setList] = useState([]);

  const changeNumber = e => setNumber(e.target.value);

  const changeList = () => {
    const newList = list.concat(number);
    setList(newList);

    setNumber("");
    refNumber.current.focus();
  };

  const refNumber = useRef();

  return (
    <>
      <div>
        <input
          ref={refNumber}
          type="number"
          value={number}
          onChange={changeNumber}
        />
        <button onClick={changeList}>등록</button>
      </div>
      <div>
        <p>입력값: {number}</p>
      </div>
      <div>
        등록된 숫자
        <ul>
          {list.map((data, index) => (
            <li key={index}>{data}</li>
          ))}
        </ul>
      </div>
    </>
  );
}

저번에 했던 이 예제에서 기능을 추가하여 useMemo를 사용하여 최적화해보자

import React, { useRef, useState } from 'react';

const getAverage = (numbers) => {
  console.log('평균값 계산 중 ...');

  // 빈 배열인 경우 0을 반환
  if (numbers.length === 0) return 0;

  // 평균을 계산 => 총합을 계산해서 배열의 길이로 나눈 값을 반환
  const total = numbers.reduce((prev, curr) => prev + curr); // reduce 메서드 : 누산기
  console.log(total);
  return total / numbers.length;
};

export default function Average() {
  const [number, setNumber] = useState('');
  const [list, setList] = useState([]);

  const changeNumber = (e) => setNumber(e.target.value);

  const changeList = () => {
    const newList = list.concat(Number(number));
    setList(newList);

    setNumber('');
    refNumber.current.focus();
  };

  const refNumber = useRef();

  return (
    <>
      <div>
        <input ref={refNumber} type="number" value={number} onChange={changeNumber} />
        <button onClick={changeList}>등록</button>
      </div>
      <div>
        <p>입력값: {number}</p>
        <p>평균값: {getAverage(list)}</p>
      </div>
      <div>
        등록된 숫자
        <ul>
          {list.map((data, index) => (
            <li key={index}>{data}</li>
          ))}
        </ul>
      </div>
    </>
  );
}

getAverage가 호출될 필요가 없는데 값을 입력할 때마다 평균값을 계속 계산하고 있다.
리스트에 숫자가 등록될 때만 호출되면 되는데..!!
=> 불필요한 연산이 계속 일어난다.

숫자를 입력하는 도중에도 불필요하게 평균값을 계산
=> 불필요한 리소스 사용 및 렌더링 지연 문제가 발생

useMemo 훅을 이용해서 리스트에 숫자가 등록된 경우에만 평균값을 계산하도록 수정

<p>평균값: {getAverage(list)}</p>
⬇️
const avg = useMemo(() => getAverage(list), [list]);
<p>평균값: {avg}</p>
이렇게 useMemo 로 list 가 변할 때만 평균값을 계산하도록 하니

import React, { useRef, useState, useMemo } from 'react';

const getAverage = (numbers) => {
  console.log('평균값 계산 중 ...');

  // 빈 배열인 경우 0을 반환
  if (numbers.length === 0) return 0;

  // 평균을 계산 => 총합을 계산해서 배열의 길이로 나눈 값을 반환
  const total = numbers.reduce((prev, curr) => prev + curr); // reduce 메서드 : 누산기
  console.log(total);
  return total / numbers.length;
};

export default function Average() {
  const [number, setNumber] = useState('');
  const [list, setList] = useState([]);

  const changeNumber = (e) => setNumber(e.target.value);

  const changeList = () => {
    const newList = list.concat(Number(number));
    setList(newList);

    setNumber('');
    refNumber.current.focus();
  };

  const refNumber = useRef();

  const avg = useMemo(() => getAverage(list), [list]);

  return (
    <>
      <div>
        <input ref={refNumber} type="number" value={number} onChange={changeNumber} />
        <button onClick={changeList}>등록</button>
      </div>
      <div>
        <p>입력값: {number}</p>
        <p>평균값: {avg}</p>
      </div>
      <div>
        등록된 숫자
        <ul>
          {list.map((data, index) => (
            <li key={index}>{data}</li>
          ))}
        </ul>
      </div>
    </>
  );
}


리스트가 변경될 때만 getAverage가 계산되어 불필요한 렌더링을 방지하였다 !!


useCallback

개념

  • useMemo 훅과 유사하게 성능 최적화를 위해 사용
  • useCallback은 콜백 함수가 불필요하게 다시 생성되는 것을 방지 => 컴포넌트가 리렌더링될 때 동일한 콜백 함수가 사용되도록 함
  • 콜백 함수가 자식 컴포넌트의 props로 전달되는 경우 유용
const memoizedCallback = useCallback(() => { ... }, [ dependency, ... ]
                                    ~~~~~~~~~~~~~~  ~~~~~~~~~~~~~~~~~~~
                                    콜백함수          의존성 배열 -> 배열의 값이 변경될 때만 콜백 함수가 새로 생성

예시 ⬇️

props로 전달되는 함수가 재정의되어 자식 컴포넌트가 리렌더링되는 것을 확인

함수가 바뀌지 않는데 부모가 리렌더링되면서 함수가 다시 만들어져서 props로 전달되어 자식이 리렌더링된다.

import { useState } from 'react';

const Todos = ({ todos, addTodo }) => {
console.log(addTodo);
console.log('Child component is rendering...');

return (

<div>
  <button onClick={addTodo}>Add Todo</button>
  <h2>Todos</h2>
  {todos.map((todo, index) => (
    <p key={index}>{todo}</p>
  ))}
</div>

);
};

export default function App() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);

const increment = () => setCount(count + 1);
const addTodo = () => {
setTodos([...todos, '할 일']);
};

return (
<>

  <hr />
  <div>
    <button onClick={increment}>카운트 증가</button>
    <h2>Count: {count}</h2>
  </div>
</>

);
}

![](https://velog.velcdn.com/images/letthem/post/b1d19bc9-f70d-4d0f-844e-72090c9b24e3/image.png)
count만 바뀌고 있는데도 불구하고 Todos가 새롭게 그려지고 있다 ㅠ.ㅠ

addTodo가 바뀔 때만 부르고 싶다.

#### React.memo 사용
```jsx
// React.memo()로 컴포넌트를 래핑하면,
// 리액트는 컴포넌트를 렌더링하고 그 결과를 메모이징(Memoizing)해
// 다음 렌더링이 일어날 때 props가 같으면 메모이징된 내용을 재사용
const Todos = React.memo(({ todos, addTodo }) => {
  console.log(addTodo);
  console.log('Child component is rendering...');

  return (
    <div>
      <button onClick={addTodo}>Add Todo</button>
      <h2>Todos</h2>
      {todos.map((todo, index) => (
        <p key={index}>{todo}</p>
      ))}
    </div>
  );
});


export default function App() {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => setCount(count + 1);
  
  // addTodo 함수의 내용이 변경되지 않았음에도 불구하고, <Todos> 컴포넌트를 리렌더링 함
  // Todos 컴포넌트에 addTodo props 남겨두고, todos props를 제거해서 확인해 볼 수 있음
  // ⬇️ count 호출될때마다 App이 리렌더링 돼서 addTodo가 계속 다시 생성되는 문제..!!
  const addTodo = () => {
    setTodos([...todos, '할 일']);
  };

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        <button onClick={increment}>카운트 증가</button>
        <h2>Count: {count}</h2>
      </div>
    </>
  );
}

이렇게 자식 컴포넌트에 React.memo() 로 감싸주면 리액트가 메모이징(Memoizing)해 다음 렌더링이 일어날 때 props가 같으면 메모이징된 내용을 재사용한다

그런데 카운트 증가를 눌러도 앞과 똑같이 리렌더링된다ㅠ 왜지?

🔑

App 함수를 호출할 때마다 addTodo가 새롭게 만들어지고 있기 때문에 todos는 그대로인데 addTodo는 매번 바뀌기 때문에 메모이징 되지 않고 매번 시행되고 있는것이다..!!!

useCallback을 이용해서 props 변수로 전달되는 함수가 매법 재정의되지 않도록 수정

  const addTodo = () => {
    setTodos([...todos, '할 일']);
  };

⬇️

  const addTodo = useCallback(() => {
    setTodos([...todos, '할 일']);
  }, [todos]);

이렇게 useCallback으로 감싸주고 todos가 변경될 때만 함수가 재정의되도록 한다.

위 예시에서는 의존성 배열을 안 써도 된다.

  • 왜냐하면 이 함수의 내용은 안 바뀌는 것이기 때문 !
  • Todos 컴포넌트에서 todos만 바뀌어도 리렌더링 되기 때문 !


count를 올리면 리렌더링되지 않고, Add Todo를 누르면 리렌더링 된다 !! ← 원하는 대로 되었다.

useCallback은 memo와 함께 사용된다.
memo를 안 쓰면 props가 변경되든 말든 계속 리렌더링되기 때문이다.

최종 코드 ⬇️

import React, { useCallback, useState } from 'react';

// React.memo()로 컴포넌트를 래핑하면,
// 리액트는 컴포넌트를 렌더링하고 그 결과를 메모이징(Memoizing)해
// 다음 렌더링이 일어날 때 props가 같으면 메모이징된 내용을 재사용
const Todos = React.memo(({ todos, addTodo }) => {
  
  console.log(addTodo);
  console.log('Child component is rendering...');

  return (
    <div>
      <button onClick={addTodo}>Add Todo</button>
      <h2>Todos</h2>
      {todos.map((todo, index) => (
        <p key={index}>{todo}</p>
      ))}
    </div>
  );
});

export default function App() {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => setCount(count + 1);
  const addTodo = useCallback(() => {
    setTodos([...todos, '할 일']);
  }, [todos]);

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        <button onClick={increment}>카운트 증가</button>
        <h2>Count: {count}</h2>
      </div>
    </>
  );
}

useContext

함수형 컴포넌트에서 Context API를 쉽게 사용할 수 있도록 도와주는 훅

Context API

  • 컴포넌트 트리에서 전역적으로 데이터를 공유할 수 있는 방법을 제공
  • 이를 통해 부모 컴포넌트에서 자식 컴포넌트로 데이터를 반복적으로 전달하는 props drilling 문제를 해결할 수 있다.
<A>
  <B v="x">
    <C v={props.v}>
      <D v={props.v}>{props.v}</D>
    </C>
  </B>
</A>

B의 v에서 D의 콘텐츠로 사용하려면 C, D 등 props를 전달받아 계속 넘겨줘야 한다.

useContext 역할

  • Context를 구독(subscribe)해 해당 Context의 현재 값을 가져오는 데 사용
  • 컴포넌트가 Context 값을 사용하면, Context의 값이 변경될 때 자동으로 해당 컴포넌트가 다시 렌더링된다.

사용법

  • Context 생성 => React.createContext() 를 사용해서 context를 생성
  • Provider 정의 => Context.Provider(<- 컴포넌트)를 사용해서 context 값을 하위 컴포넌트로 전달
  • Context 소비 => useContext 훅을 사용해서 context 값을 가져와서 사용

예시 ⬇️

테마가 적용된 페이지를 생성

ThemedButton, Blog, News 컴포넌트를 생성 ⇒ 테마 변경 버튼을 클릭하면 버튼의 배경색과 글자색을 변경

import React, { useState } from 'react';

const ThemedButton = () => {
  const [theme, setTheme] = useState('light');
  const changeTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
  return (
    <button
      onClick={changeTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : 'yellow',
      }}
    >
      테마 변경
    </button>
  );
};

const Blog = () => {
  return (
    <div>
      <h1>블로그</h1>
      <hr />
      <h2>블로그 제목</h2>
      <p>블로그 내용</p>
    </div>
  );
};

const News = () => {
  return (
    <div>
      <h1>뉴스</h1>
      <hr />
      <h2>뉴스 제목</h2>
      <p>뉴스 내용</p>
    </div>
  );
};

export default function App() {
  return (
    <div>
      <h1>테마가 적용된 페이지</h1>
      <ThemedButton />
      <Blog />
      <News />
    </div>
  );
}

테마가 각 컴포넌트(블로그, 뉴스)에 적용될 수 있도록 변경

상태변수와 상태변수를 변경하는 함수를 부모 컴포넌트에 정의하고 각각 자식 컴포넌트의 props 변수로 전달

import { useState } from 'react';
import './App.css';

const ThemedButton = ({ theme, changeTheme }) => {
  /*
  const [theme, setTheme] = useState("light");
  const changeTheme = () => setTheme(theme === "light" ? "dark" : "light");
  */
  return (
    <button
      onClick={changeTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : 'yellow',
      }}
    >
      테마 변경
    </button>
  );
};

const Blog = ({ theme }) => {
  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      <h1>블로그</h1>
      <hr />
      <h2>블로그 제목</h2>
      <p>블로그 내용</p>
    </div>
  );
};

const News = ({ theme }) => {
  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      <h1>뉴스</h1>
      <hr />
      <h2>뉴스 제목</h2>
      <p>뉴스 내용</p>
    </div>
  );
};

export default function App() {
  const [theme, setTheme] = useState('light');
  const changeTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');

  return (
    <div>
      <h1>테마가 적용된 페이지</h1>
      <ThemedButton theme={theme} changeTheme={changeTheme} />
      <Blog theme={theme} />
      <News theme={theme} />
    </div>
  );
}

Blog와 News를 포함하는 Contents 컴포넌트를 추가

😭 Contents는 theme 가 필요하지도 않는데 theme를 받아서 Blog와 News에 전달해줘야 한다
=> Contents 컴포넌트는 theme 변수를 필요로 하지 않지만, 하위 컴포넌트에게 전달하기 위해서 props 변수로 받아서 처리

import { useState } from 'react';
import './App.css';

const ThemedButton = ({ theme, changeTheme }) => {
  /*
  const [theme, setTheme] = useState("light");
  const changeTheme = () => setTheme(theme === "light" ? "dark" : "light");
  */
  return (
    <button
      onClick={changeTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : 'yellow',
      }}
    >
      테마 변경
    </button>
  );
};

const Blog = ({ theme }) => {
  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      <h1>블로그</h1>
      <hr />
      <h2>블로그 제목</h2>
      <p>블로그 내용</p>
    </div>
  );
};

const News = ({ theme }) => {
  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      <h1>뉴스</h1>
      <hr />
      <h2>뉴스 제목</h2>
      <p>뉴스 내용</p>
    </div>
  );
};

const Contents = ({ theme }) => {
  return (
    <>
      <Blog theme={theme} />
      <News theme={theme} />
    </>
  );
};
export default function App() {
  const [theme, setTheme] = useState('light');
  const changeTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');

  return (
    <div>
      <h1>테마가 적용된 페이지</h1>
      <ThemedButton theme={theme} changeTheme={changeTheme} />
      <Contents theme={theme} />
    </div>
  );
}

useContext 훅을 이용해서 수정

import { createContext, useContext, useState } from 'react';
import './App.css';

// #1 Context 생성
const ThemeContext = createContext();

// #2-1. Provider 정의
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  const changeTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');

  return <ThemeContext.Provider value={{ theme, changeTheme }}>{children}</ThemeContext.Provider>;
};

//const ThemedButton = ({ theme, changeTheme }) => {
const ThemedButton = () => {
  // #3 Context 소비
  const { theme, changeTheme } = useContext(ThemeContext);
  return (
    <button
      onClick={changeTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : 'yellow',
      }}
    >
      테마 변경
    </button>
  );
};

// const Blog = ({ theme }) => {
const Blog = () => {
  // #3 Context 소비
  const { theme } = useContext(ThemeContext);
  
  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      <h1>블로그</h1>
      <hr />
      <h2>블로그 제목</h2>
      <p>블로그 내용</p>
    </div>
  );
};

// const News = ({ theme }) => {
const News = () => {
  // #3 Context 소비
  const { theme } = useContext(ThemeContext);
  
  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      <h1>뉴스</h1>
      <hr />
      <h2>뉴스 제목</h2>
      <p>뉴스 내용</p>
    </div>
  );
};

/* 😇 하위 컴포넌트로 전달을 위한 props 변수 생성(정의)하지 않아도 됨 
const Contents = ({ theme }) => {
  return (
    <>
      <Blog theme={theme} />
      <News theme={theme} />
    </>
  );
};
*/
const Contents = () => {
  return (
    <>
      <Blog />
      <News />
    </>
  );
};

// 2-2. context 변수를 공유할 컴포넌트를 Provider로 둘러싼다.
export default function App() {
  /* Provider에서 정의
  const [theme, setTheme] = useState('light');
  const changeTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
  */

  /* ThemeProvider로 감싸고, props 변수를 삭제 */
  return (
    <ThemeProvider>
      <div>
        <h1>테마가 적용된 페이지</h1>
        <ThemedButton />
        <Contents />
      </div>
    </ThemeProvider>
  );
}

참고) 위 코드의 #2-1, #2-2를 아래와 같이 App에서 직접 표현할 수도 있음

=

export default function App() {
  
  const [theme, setTheme] = useState('light');
  const changeTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
  

  return (
    <ThemeContext.Provider value={{ theme, changeTheme }}>
      <div>
        <h1>테마가 적용된 페이지</h1>
        <ThemedButton />
        <Contents />
      </div>
    </ThemeContext.Provider>
  );
}

컴포넌트를 파일(모듈)로 분리

ThemeContext.js

import { createContext } from 'react';

// #1 Context 생성
const ThemeContext = createContext();

export default ThemeContext;

ThemeProvider.js

import { useState } from 'react';
import ThemeContext from './ThemeContext';

// #2 Provider 정의
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  const changeTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');

  return <ThemeContext.Provider value={{ theme, changeTheme }}>{children}</ThemeContext.Provider>;
};

export default ThemeProvider;

여기서 {{ theme, changeTheme }} {표현식{객체}} 단축속성명
객체에서 theme={theme}, changeTheme={changeTheme} 를 축약해서 나타낸 것

ThemedButton.js

import { useContext } from 'react';
import ThemeContext from './ThemeContext';

const ThemedButton = () => {
  // #3 Context 소비
  const { theme, changeTheme } = useContext(ThemeContext);
  return (
    <button
      onClick={changeTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : 'yellow',
      }}
    >
      테마 변경
    </button>
  );
};

export default ThemedButton;

Blog.js

import { useContext } from 'react';
import ThemeContext from './ThemeContext';

const Blog = () => {
  const { theme } = useContext(ThemeContext);

  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      <h1>블로그</h1>
      <hr />
      <h2>블로그 제목</h2>
      <p>블로그 내용</p>
    </div>
  );
};

export default Blog;

News.js

import { useContext } from 'react';
import ThemeContext from './ThemeContext';

const News = () => {
  const { theme } = useContext(ThemeContext);

  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff',
      }}
    >
      <h1>뉴스</h1>
      <hr />
      <h2>뉴스 제목</h2>
      <p>뉴스 내용</p>
    </div>
  );
};

export default News;

Contents.js

import Blog from "./Blog";
import News from "./News";

const Contents = () => {
  return (
    <>
      <Blog />
      <News />
    </>
  );
};

export default Contents;

App.js

import './App.css';
import ThemeProvider from './ThemeProvider';
import ThemedButton from './ThemedButton';
import Contents from './Contents';

export default function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>테마가 적용된 페이지</h1>
        <ThemedButton />
        <Contents />
      </div>
    </ThemeProvider>
  );
}

App.js 를 ThemeProvider 컴포넌트를 사용하지 않고 Provider를 직접 정의하는 방식으로 변경

내 실습 코드 ⬇️

import './App.css';
import ThemedButton from './ThemedButton';
import Contents from './Contents';
import ThemeContext from './ThemeContext';
import { useState } from 'react';

export default function App() {
  const [theme, setTheme] = useState('light');
  const changeTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');

  return (
    <ThemeContext.Provider value={{ theme, changeTheme }}>
      <div>
        <h1>테마가 적용된 페이지</h1>
        <ThemedButton />
        <Contents />
      </div>
    </ThemeContext.Provider>
  );
}

[실습 1]

아래 코드의 상태변수와 상태변수를 변경하는 함수를 App에 정의하고, 하위 컴포넌트에서 useContext 훅을 이용해서 사용하는 방법으로 변경

import { useState } from "react";

function Child({ plusOne, subCount }) {
  return (
    <>
      <button onClick={plusOne}>+1</button>
      <button onClick={subCount}>-1</button>
    </>
  );
};

function Parent() {
  const [count, setCount] = useState(0);
  const addCount = () => setCount(count + 1);
  const subCount = () => setCount(count - 1);
  const resetCount = () => setCount(0);
  return (
    <>
      <div>{count}</div>
      <button onClick={resetCount}>Reset</button>
      <Child plusOne={addCount} subCount={subCount} />
    </>
  );
}

function App() {
  return (
    <Parent />
  );
}
export default App;

수정 결과 ⬇️

import { createContext, useContext, useState } from 'react';

function Child() {
  const { addCount: plusOne, subCount } = useContext(CounterContext);
  return (
    <>
      <button onClick={plusOne}>+1</button>
      <button onClick={subCount}>-1</button>
    </>
  );
}

function Parent() {
  // #3 props 변수로 전달하는 또는 전달 받는 코드를 삭제하고,
  //    useContext() 훅을 이용해서 필요한 컨텍스트 변수를 추출해서 사용
  const { count, resetCount } = useContext(CounterContext);
  return (
    <>
      <div>{count}</div>
      <button onClick={resetCount}>Reset</button>
      <Child />
    </>
  );
}

// #1 컨텍스트 생성
const CounterContext = createContext();

// #2 Provider 생성
const CounterProvider = ({ children }) => {
  const [count, setCount] = useState(0);
  const addCount = () => setCount(count + 1);
  const subCount = () => setCount(count - 1);
  const resetCount = () => setCount(0);

  return (
    <CounterContext.Provider value={{ count, addCount, subCount, resetCount }}>
      {children}
    </CounterContext.Provider>
  );
};

// #2-1 Provider 적용
function App() {
  return (
    <CounterProvider>
      <Parent />
    </CounterProvider>
  );
}
export default App;

[실습 2]

import { useState } from "react";

function Controller({ plusOne, minusOne }) {
  return (
    <>
      <button onClick={plusOne}> +1 </button>
      <button onClick={minusOne}> -1 </button>
    </>
  );
}

function Display({ count }) {
  return (
    <h1>{count}</h1>
  );
}

function Parent() {
  const [count, setCount] = useState(0);
  const reset = () => setCount(0);
  const plusOne = () => setCount(count + 1);
  const minusOne = () => setCount(count - 1);
  return (
    <>
      <button onClick={reset}>Reset</button>
      <Controller plusOne={plusOne} minusOne={minusOne} />
      <Display count={count} />
    </>
  );
}

function App() {
  return (
    <Parent />
  );
}
export default App;

수정 결과 ⬇️

import { createContext, useContext, useState } from 'react';

// #3 props 변수를 제거하고, useContext 훅을 이용해서 컨텍스트 변수를 가져와서 사용
function Controller() {
  const { plusOne, minusOne } = useContext(CounterContext);
  return (
    <>
      <button onClick={plusOne}> +1 </button>
      <button onClick={minusOne}> -1 </button>
    </>
  );
}

function Display() {
  const { count } = useContext(CounterContext);
  return <h1>{count}</h1>;
}

function Parent() {
  const { reset } = useContext(CounterContext);
  return (
    <>
      <button onClick={reset}>Reset</button>
      <Controller />
      <Display />
    </>
  );
}

// #1 컨텍스트 생성
const CounterContext = createContext();

// #2-1 프로바이더 생성
const CounterProvider = ({ children }) => {
  const [count, setCount] = useState(0);
  const reset = () => setCount(0);
  const plusOne = () => setCount(count + 1);
  const minusOne = () => setCount(count - 1);

  return (
    <CounterContext.Provider value={{ count, reset, plusOne, minusOne }}>
      {children}
    </CounterContext.Provider>
  );
};

// #2-2 프로바이더를 적용
function App() {
  return (
    <CounterProvider>
      <Parent />
    </CounterProvider>
  );
}
export default App;

참고) useReducer을 이용해서 상태변수 변경 로직을 단순화

이 예제에는 적합하지 않지만 적용시켜봄 !

import { createContext, useContext, useReducer, useState } from 'react';

function Controller() {
  const { plusOne, minusOne } = useContext(CounterContext);
  return (
    <>
      <button onClick={plusOne}> +1 </button>
      <button onClick={minusOne}> -1 </button>
    </>
  );
}

function Display() {
  const { count } = useContext(CounterContext);
  return <h1>{count}</h1>;
}

function Parent() {
  const { reset } = useContext(CounterContext);
  return (
    <>
      <button onClick={reset}>Reset</button>
      <Controller />
      <Display />
    </>
  );
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'reset':
      return { count: 0 };
    case 'plusone':
      return { count: state.count + 1 };
    case 'minusone':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const CounterContext = createContext();

const CounterProvider = ({ children }) => {
  /*
  const [count, setCount] = useState(0);
  const reset = () => setCount(0);
  const plusOne = () => setCount(count + 1);
  const minusOne = () => setCount(count - 1);
  */
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <CounterContext.Provider
      value={{
        count: state.count,
        reset: () => dispatch({ type: 'reset' }),
        plusOne: () => dispatch({ type: 'plusone' }),
        minusOne: () => dispatch({ type: 'minusone' }),
      }}
    >
      {children}
    </CounterContext.Provider>
  );
};

function App() {
  return (
    <CounterProvider>
      <Parent />
    </CounterProvider>
  );
}
export default App;

커스텀 Hook 만들기

hook : 외부에 정의해서 내부에 넘겨주는 것
나의 필요에 의해서 만들어 쓸 수 있다

useReducer를 이용해서 여러 상태 변수를 관리하던 것을 사용자 정의 훅으로 대체

import { useReducer, useState } from "react";

function reducer(state, action) {
  return {
    ...state, 
    [action.name]: action.value
  };
}

function Info() {
    const [state, dispatch] = useReducer(reducer, {name: '', nickname: ''});
    const {name, nickname} = state;
    const handlerChange = (e) => {
      dispatch(e.target);
    };

    return (
        <>
            <div>
                <p>이름: {name}</p>
                <p>별명: {nickname}</p>
            </div>
            <div>
                <p>이름: <input type="text" value={name} name="name" onChange={handlerChange} /></p>
                <p>별명: <input type="text" value={nickname} name="nickname" onChange={handlerChange} /></p>
            </div>
        </>
    );
}

const App = () => {
  return (
    <>
      <Info />
    </>
  );
};

export default App;

상태변수와 상태변수의 값을 변경하는 핸들러 함수를 반환하는 useInputs 사용자 정의 훅 함수 생성

useInputs 를 가져와서 쓰기만 하면 되므로 편하다!

import { useReducer, useState } from 'react';

function reducer(state, action) {
  return {
    ...state,
    [action.name]: action.value,
  };
}

function useInputs(initState) {
  const [state, dispatch] = useReducer(reducer, initState);
  const handlerChange = (e) => {
    dispatch(e.target);
  };
  return [state, handlerChange];
}

function Info() {
  /*
  const [state, dispatch] = useReducer(reducer, {name: '', nickname: ''});
  const {name, nickname} = state;
  const handlerChange = (e) => {
    dispatch(e.target);
  };
  */

  const [state, handlerChange] = useInputs({ name: '', nickname: '' });
  const { name, nickname } = state;

  return (
    <>
      <div>
        <p>이름: {name}</p>
        <p>별명: {nickname}</p>
      </div>
      <div>
        <p>
          이름: <input type="text" value={name} name="name" onChange={handlerChange} />
        </p>
        <p>
          별명: <input type="text" value={nickname} name="nickname" onChange={handlerChange} />
        </p>
      </div>
    </>
  );
}

const App = () => {
  return (
    <>
      <Info />
    </>
  );
};

export default App;

일정관리 앱 개발

프로젝트 생성

npx create-react-app todo-app
cd todo-app
npm install react@18 react-dom@18
npm install web-vitals
npm install react-icons classnames
npm start

프로젝트에 맞도록 초기화

index.css

body {
  margin: 0;
  padding: 0;
  background-color: #e9ecef;
}

App.js

import './App.css';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

function App() {
  return (
    <TodoTemplate>
      <TodoInsert />
      <TodoList />
    </TodoTemplate>
  );
}

export default App;

src 디렉터리 아래에 components 디렉터리를 생성

TodoTemplate.js

import './TodoTemplate.css';

const TodoTemplate = (props) => {
  return (
    <>
      <div className="TodoTemplate">
        <div className="appTitle">일정 관리</div>
        <div className="content">{props.children}</div>
      </div>
    </>
  );
};
export default TodoTemplate;

TodoInsert.js

아이콘

import './TodoInsert.css';
import { MdAdd } from 'react-icons/md';

const TodoInsert = () => {
  return (
    <form className="TodoInsert">
      <input type="text" placeholder="할일을 입력하세요." />
      <button type="submit">
        <MdAdd />
      </button>
    </form>
  );
};
export default TodoInsert;

TodoList.js

import TodoListItem from './TodoListItem';
import './TodoList.css';

export default function TodoList() {
  return (
    <div>
      <TodoListItem />
      <TodoListItem />
      <TodoListItem /> 
      <TodoListItem />
      <TodoListItem />
      <TodoListItem />
      <TodoListItem />
      <TodoListItem />
      <TodoListItem /> 
      <TodoListItem />
      <TodoListItem />
      <TodoListItem />
    </div>
  );
}

TodoListItem.js

import './TodoListItem.css';
import { MdCheckBoxOutlineBlank, MdRemoveCircleOutline } from 'react-icons/md';

const TodoListItem = () => {
  return (
    <div className="TodoListItem">
      <div className="checkBox">
        <MdCheckBoxOutlineBlank />
        <div className="text">할 일 내용</div>
      </div>
      <div className="remove">
        <MdRemoveCircleOutline />
      </div>
    </div>
  );
};

export default TodoListItem;

0개의 댓글