[React] State 관리(2)

MinJi·2024년 8월 14일

FrontEnd

목록 보기
8/13

State를 보존하고 초기화하기

  • React는 같은 컴포넌트가 같은 자리에 렌더링되는 한 state를 유지한다.
const [isFancy, setIsFancy] = useState(false);

{isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
)}

  • state는 JSX 태그에 저장되지 않으며, JSX으로 만든 트리 위치와 연관된다.
const [isPaused, setIsPaused] = useState(false);
{isPaused ? (
        <p>See you later!</p> 
      ) : (
        <Counter /> 
)}

  • 컴포넌트에 다른 key를 주어서 그 하위 트리를 초기화하도록 강제할 수 있다.
const [isPlayerA, setIsPlayerA] = useState(true);

{isPlayerA ? (
        <Counter key="Taylor" person="Taylor" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
)}
  • 중첩해서 컴포넌트를 정의하면 원치 않게 state가 초기화될 수 있다.

state 로직을 reducer로 작성하기

  • 한 컴포넌트에서 state 업데이트가 여러 이벤트 핸들러로 분산되면 컴포넌트 관리가 어렵기 때문에 state를 업데이트하는 모든 로직을 reducer를 사용해 컴포넌트 외부에 단일 함수로 통합해 관리할 수 있다.

예시 코드

import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([...tasks, {
      id: nextId++,
      text: text,
      done: false
    }]);
  }

  function handleChangeTask(task) {
    setTasks(tasks.map(t => {
      if (t.id === task.id) {
        return task;
      } else {
        return t;
      }
    }));
  }

  function handleDeleteTask(taskId) {
    setTasks(
      tasks.filter(t => t.id !== taskId)
    );
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}
  1. action을 dispatch 함수로 전달하는 것으로 바꾼다.
  • 기존 이벤트 핸들러
    - 사용자가 “Add” 를 눌렀을 때 호출되는 handleAddTask(text)
    - 사용자가 task를 토글하거나 “저장”을 누르면 호출되는 handleChangeTask(task)
    - 사용자가 “Delete” 를 누르면 호출되는 handleDeleteTask(taskId)

다음과 같은 형태로 변경한다.

dispatch({
  // 컴포넌트마다 다른 값
  type: 'what_happened',
  // 다른 필드는 이곳에
});

변경 코드

function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text: text,
  });
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task: task
  });
}

function handleDeleteTask(taskId) {
  dispatch({
    type: 'deleted',
    id: taskId
  });
}
  1. reducer 함수를 작성한다.
  • reducer 함수는 현재의 state 값과 action 객체를 인자로 받고 다음 state 값을 반환한다.

다음과 같은 형태로 작성한다.

function yourReducer(state, action) {
  // React가 설정하게될 다음 state 값을 반환합니다.
}

추가 코드

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}
  1. 컴포넌트에서 reducer를 사용한다.
// 기존
const [tasks, setTasks] = useState(initialTasks);

// 변경
import { useReducer } from 'react';

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useState VS useReducer

useState useReducer
코드 크기 미리 작성해야 하는 코드가 줄어든다. reducer함수와 action 전달 부분을 작성해야하지만, 여러 이벤트 핸들러에서 비슷한 방식으로 state를 업데이트 하는 경우 코드의 양을 줄이는데 도움이 된다.
가독성 간단한 state를 업데이트 하는 경우 가독성이 좋은 편이다. 업데이트 로직이 어떻게 동작하는지와 이벤트 핸들러를 통해서 무엇이 발생했는지 명확하게 구분할 수 있다.
디버깅 왜, 어디서 state가 잘못 설정됐는지 찾기 어렵다. 콘솔 로그를 추가하여 확인 할 수 있다. 더 많은 코드를 단계별로 실행해서 디버깅해야 한다.

Context

  • 명시적으로 props를 전달해주지 않아도 부모 컴포넌트가 트리에 있는 어떤 자식 컴포넌트에서나 정보를 사용할 수 있다.

Context로 데이터 전달하기

  1. Context 생성
// LevelContext.js
import { createContext } from 'react';

export const LevelContext = createContext(1);
  1. Context 사용
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}

<Section level={4}>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
</Section>
  • useContext는 Hook이다.
  • React 컴포넌트의 바로 안에서만 호출할 수 있다.
  1. Context 제공
import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

같은 컴포넌트에서 context를 사용하며 제공하기

  • Context를 통해 위의 컴포넌트에서 정보를 읽을 수 있으므로 각 Section은 위의 Section에서 level을 읽고 자동으로 level+1을 아래로 전달할 수 있다.
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);
  return (
    <section className="section">
      <LevelContext.Provider value={level + 1}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

Context 사용 예시

  • 테마 지정하기: 사용자가 모양을 변경할 수 있는 애플리케이션의 경우에 (e.g. 다크 모드) context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 컴포넌트에서 context를 사용할 수 있다.
  • 현재 계정: 로그인한 사용자를 알아야 하는 컴포넌트가 많은 경우, context에 놓으면 트리 어디에서나 편하게 알아낼 수 있다. 일부 애플리케이션에서는 동시에 여러 계정을 운영할 수도 있다(e.g. 다른 사용자로 댓글을 남기는 경우). 이런 경우에는 UI의 일부를 서로 다른 현재 계정 값을 가진 provider로 감싸 주는 것이 편리하다.
  • 라우팅: 대부분의 라우팅 솔루션은 현재 경로를 유지하기 위해 내부적으로 context를 사용한다.
  • 상태 관리: 애플리케이션이 커지면 결국 앱 상단에 수많은 state가 생기게 되고 멀리 떨어진 많은 컴포넌트가 그 값을 변경하고싶어할 수 있다. 흔히 reducer를 context와 함께 사용하는 것은 복잡한 state를 관리하고 번거로운 작업 없이 멀리 있는 컴포넌트까지 값을 전달하는 방법이다.

Reducer와 Context로 앱 확장하기

  1. Context를 생성한다.
import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
  1. State과 dispatch 함수를 context에 넣는다.
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  // ...
  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        ...
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}
  1. 트리 안에서 context를 사용한다.
  • tasks 리스트나 이벤트 핸들러를 트리 아래로 전달할 필요가 없다.
<TasksContext.Provider value={tasks}>
  <TasksDispatchContext.Provider value={dispatch}>
    <h1>Day off in Kyoto</h1>
    <AddTask />
    <TaskList />
  </TasksDispatchContext.Provider>
</TasksContext.Provider>
  • 필요한 컴포넌트에서는 TaskContext에서 tasks 리스트를 읽을 수 있다.
export default function TaskList() {
  const tasks = useContext(TasksContext);
  // ...
  • tasks 리스트를 업데이트하기 위해서 컴포넌트에서 context의 dispatch 함수를 읽고 호출할 수 있다.
export default function AddTask({ onAddTask }) {
  const [text, setText] = useState('');
  const dispatch = useContext(TasksDispatchContext);
  // ...
  return (
    // ...
    <button onClick={() => {
      setText('');
      dispatch({
        type: 'added',
        id: nextId++,
        text: text,
      });
    }}>Add</button>
    // ...

0개의 댓글