๐Ÿ” Context

๊น€์ˆ˜์ง„ยท2025๋…„ 7์›” 10์ผ

ํ•œ ์ž… ํฌ๊ธฐ๋กœ ์ž˜๋ผ ๋จน๋Š” ๋ฆฌ์•กํŠธ ์„น์…˜ 12 - Context

React Context

์ปดํฌ๋„ŒํŠธ๊ฐ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๋˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์ด๋‹ค.
๊ธฐ์กด์˜ props๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋˜ ๋‹จ์  (์˜ค์ง ์ง๊ณ„ ๋ถ€๋ชจ์—์„œ ์ž์‹์œผ๋กœ๋งŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌ)์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. (๋น„์œ : props๋Š” ํ• ์•„๋ฒ„์ง€๊ฐ€ ์†์ž์—๊ฒŒ ์ „๋‹ฌ ๋ชปํ•˜๊ณ  ์•„๋ฒ„์ง€๋ฅผ ํ†ตํ•ด ์ „๋‹ฌ๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค.)
context๋Š” ๋ฐ์ดํ„ฐ ๋ณด๊ด€์†Œ์ด๋‹ค. ์ด ๊ณณ์— ๋ณด๊ด€ํ•˜์—ฌ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

์ด๋Ÿฐ์‹์œผ๋กœ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ค€๋‹ค.

App.jsx

import "./App.css";
import {
  useState,
  useRef,
  useReducer,
  useCallback,
  createContext,
} from "react";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";

const mockData = [
  {
    id: 0,
    isDone: false,
    content: "React ๊ณต๋ถ€ํ•˜๊ธฐ",
    date: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "์ฝ”์–ดํƒ€์ž„ ๊ฐ–๊ธฐ",
    date: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "์ผ์ฐ ์ผ์–ด๋‚˜๊ธฐ",
    date: new Date().getTime(),
  },
];

function reducer(state, action) {
  switch (action.type) {
    case "CREATE":
      return [action.data, ...state];
    case "UPDATE":
      return state.map((item) =>
        item.id === action.targetId ? { ...item, isDone: !item.isDone } : item
      );
    case "DELETE":
      return state.filter((item) => item.id !== action.targetId); 
    default:
      return state;
  }
}

export const TodoContext = createContext();

function App() {
  const [todos, dispatch] = useReducer(reducer, mockData);
  const idRef = useRef(3);

  const onCreate = useCallback((content) => {
    dispatch({
      type: "CREATE",
      data: {
        id: idRef.current++,
        isDone: false,
        content: content,
        date: new Date().getTime(),
      },
    });
  }, []);

  const onUpdate = useCallback((targetId) => {
    dispatch({
      type: "UPDATE",
      targetId: targetId,
    });
  }, []);

  const onDelete = useCallback((targetId) => {
    dispatch({
      type: "DELETE",
      targetId: targetId,
    });
  }, []);

  return (
    <div className="App">
      <Header />
      <TodoContext.Provider value={{ todos, onCreate, onUpdate, onDelete }}>
        <Editor onCreate={onCreate} />
        <List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
      </TodoContext.Provider>
    </div>
  );
}

export default App;

Editor.jsx ....

import "./Editor.css";
import { useState, useRef, useContext } from "react";
import { TodoContext } from ".../App";

const Editor = () => {
  const { onCreate } = useContext(TodoContext);
  const [content, setContent] = useState("");
  const inputRef = useRef();

  const onChangeContent = (e) => {
    setContent(e.target.value);
  };
  const onKeydown = (e) => {
    if (e.keyCode === 13) {
      onSubmit();
    }
  };
  const onSubmit = () => {
    if (content === "") {
      inputRef.current.focus();
      return;
    }
    onCreate(content);
    setContent("");
  };

  return (
    <div className="Editor">
      <input
        ref={inputRef}
        value={content}
        onKeyDown={onKeydown}
        onChange={onChangeContent}
        placeholder="์ƒˆ๋กœ์šด Todo..."
      />
      <button onClick={onSubmit}>์ถ”๊ฐ€</button>
    </div>
  );
};

export default Editor;

์ด๋Ÿฐ ์‹์œผ๋กœ ๋‚˜๋จธ์ง€ ๊ธฐ๋Šฅ๋“ค๋„ ์ž‘์„ฑํ•ด์ค€๋‹ค.

Context ๋ถ„๋ฆฌํ•˜๊ธฐ

context ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•œ๋‹ค. ์ตœ์ ํ™”๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์•˜๋‹ค! ๋”ฐ๋ผ์„œ context ๋ถ„๋ฆฌ ํ•„์š”!

๋ถˆ๋ณ€๊ฐ’๊ณผ ๊ฐ€๋ณ€๊ฐ’์„ ๋ถ„๋ฆฌํ•œ๋‹ค.

์ตœ์ข…์ ์œผ๋กœ ์ด๋ ‡๊ฒŒ ์ž‘๋™ํ•˜๋„๋ก ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค.

์ตœ์ข… ์ฝ”๋“œ:

Editor.jsx

import "./Editor.css";
import { useState, useRef, useContext } from "react";
import { TodoDispatchContext } from "../App";

const Editor = () => {
  const { onCreate } = useContext(TodoDispatchContext);
  const [content, setContent] = useState("");
  const inputRef = useRef();

  const onChangeContent = (e) => {
    setContent(e.target.value);
  };
  const onKeydown = (e) => {
    if (e.keyCode === 13) {
      onSubmit();
    }
  };
  const onSubmit = () => {
    if (content === "") {
      inputRef.current.focus();
      return;
    }
    onCreate(content);
    setContent("");
  };

  return (
    <div className="Editor">
      <input
        ref={inputRef}
        value={content}
        onKeyDown={onKeydown}
        onChange={onChangeContent}
        placeholder="์ƒˆ๋กœ์šด Todo..."
      />
      <button onClick={onSubmit}>์ถ”๊ฐ€</button>
    </div>
  );
};

export default Editor;

Exam.jsx

import { useReducer } from "react";

function reducer(state, action) {
  switch (action.tye) {
    case "INCREASE":
      return state + action.data;
    case "Decrease":
      return state - action.data;
    default:
      return state;
  }
}

const Exam = () => {
  const [state, dispatch] = useReducer(reducer, 0);

  const onClickPlus = () => {
    dispatch({
      type: "INCREASE",
      data: 1,
    });
  };

  const onClickMinus = () => {
    dispatch({
      type: "DECREASE",
      data: 1,
    });
  };

  return (
    <div>
      <h1>{state}</h1>
      <button>onClick={onClickPlus}</button>
      <button onClick={onClickMinus}>-</button>
    </div>
  );
};

export default Exam;

Header.jsx

import "./Header.css";
import { memo } from "react";

const Header = () => {
  return (
    <div className="Header">
      <h3>์˜ค๋Š˜์€ ๐Ÿ“† </h3>
      <h1>{new Date().toDateString()}</h1>
    </div>
  );
};
const memoizedHeader = memo(Header);

export default memoizedHeader; //window + . ๋กœ ์ด๋ชจ์ง€ ์ถ”๊ฐ ๊ฐ€๋Šฅํ•˜๋‹ค.

List.jsx

import "./List.css";
import TodoItem from "./TodoItem";
import { useState, useMemo, useContext } from "react";
import { TodoStateContext } from "../App";

const List = () => {
  const todos = useContext(TodoStateContext);
  const [search, setSearch] = useState("");

  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  };

  const getFilteredData = () => {
    if (search === "") {
      return todos;
    }
    return todos.filter((todo) =>
      todo.content.toLowerCase().includes(search.toLowerCase())
    );
  };

  const filteredTodos = getFilteredData();

  const { totalCount, doneCount, notDoneCount } = useMemo(() => {
    const totalCount = todos.length;
    const doneCount = todos.filter((todo) => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return { totalCount, doneCount, notDoneCount };
  }, [todos]);

  return (
    <div className="List">
      <h4>Todo List ๐ŸŒฑ</h4>
      <div>
        <div>total: {totalCount}</div>
        <div>done: {doneCount}</div>
        <div>notDone: {notDoneCount}</div>
      </div>
      <input
        value={search}
        onChange={onChangeSearch}
        placeholder="๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
      />
      <div className="todos_wrapper">
        {filteredTodos.map((todo) => (
          <TodoItem key={todo.id} {...todo} />
        ))}
      </div>
    </div>
  );
};

export default List;

TodoItem.jsx

import "./TodoItem.css";
import { memo, useContext } from "react";
import { TodoDispatchContext } from "../App";

const TodoItem = ({ id, isDone, content, date }) => {
  const { onUpdate, onDelete } = useContext(TodoDispatchContext);

  const onChangeCheckbox = () => {
    onUpdate(id);
  };

  const onClickDeleteButton = () => {
    onDelete(id);
  };

  return (
    <div className="TodoItem">
      <input
        onChange={onChangeCheckbox}
        readOnly
        checked={isDone}
        type="checkbox"
      />
      <div className="content">{content}</div>
      <div className="date">{new Date(date).toLocaleDateString()}</div>
      <button onClick={onClickDeleteButton}>์‚ญ์ œ</button>
    </div>
  );
};

export default memo(TodoItem);

App.jsx

import "./App.css";
import {
  useState,
  useRef,
  useReducer,
  useCallback,
  createContext,
  useMemo
} from "react";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";

const mockData = [
  {
    id: 0,
    isDone: false,
    content: "React ๊ณต๋ถ€ํ•˜๊ธฐ",
    date: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "์ฝ”์–ดํƒ€์ž„ ๊ฐ–๊ธฐ",
    date: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "์ผ์ฐ ์ผ์–ด๋‚˜๊ธฐ",
    date: new Date().getTime(),
  },
];

function reducer(state, action) {
  switch (action.type) {
    case "CREATE":
      return [action.data, ...state];
    case "UPDATE":
      return state.map((item) =>
        item.id === action.targetId ? { ...item, isDone: !item.isDone } : item
      );
    case "DELETE":
      return state.filter((item) => item.id !== action.targetId); // โœ… ์ด ๋ถ€๋ถ„ ์ˆ˜์ •
    default:
      return state;
  }
}

export const TodoStateContext = createContext();
export const TodoDispatchContext = createContext();


function App() {
  const [todos, dispatch] = useReducer(reducer, mockData);
  const idRef = useRef(3);

  const onCreate = useCallback((content) => {
    dispatch({
      type: "CREATE",
      data: {
        id: idRef.current++,
        isDone: false,
        content: content,
        date: new Date().getTime(),
      },
    });
  }, []);

  const onUpdate = useCallback((targetId) => {
    dispatch({
      type: "UPDATE",
      targetId: targetId,
    });
  }, []);

  const onDelete = useCallback((targetId) => {
    dispatch({
      type: "DELETE",
      targetId: targetId,
    });
  }, []);

  const memoizedDispatch = useMemo(()=> {
    return {onCreate, onUpdate, onDelete};
  }, []);

  return (
    <div className="App">
      <Header />
      <TodoStateContext.Provider value={ todos}>
        <TodoDispatchContext.Provider
          value={memoizedDispatch}
        >
          <Editor/>
          <List/>
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>
  );
}

export default App;

์ตœ์ข… ๊ตฌํ˜„ ํ™”๋ฉด

0๊ฐœ์˜ ๋Œ“๊ธ€