Home [REACT] Context
Post
Cancel

[REACT] Context

Context

1. Context

  1. React Context
    • 컴포넌트 간 데이터를 전달하는 또 다른 방법
    • 기존의 Props가 갖고있던 단점 해결 가능
  • Props 의 단점?

Image

  • 부모 -> 자식으로만 전달이 가능하다. (Props Drilling)
    • App에서 TodoItem으로 바로 Props 전달이 불가하여 List가 중간다리 역할을 해줘야했다.

Image

  • Context는 데이터 보관소 역할을 하는 객체.

Image

  • 여러개 생성도 가능
  • Context 내부에는 많은 Property가 있지만 대부분 내부적인 동작을 위해 존재하기 때문에 Provider 만 제대로 알아두면 된다
  1. Provider
    • Provider: 공급자, 제공자
    • Context가 공급할 데이터를 설정하거나 Context의 데이터를 공급받을 컴포넌트들을 설정하기 위해 사용하는 프로퍼티.
      • Provider는 컴포넌트로 작성하여 렌더링할 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 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";

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();
console.log(TodoContext);

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

  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=>
        <Editor/>
        <List/>
      </TodoContext.Provider>
    </div>
  );
}

export default App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Editor.jsx
import './css/Editor.css';
import { useState, useRef, useContext } from "react";
import { TodoContext } from "../App";

const Editor = () => {
  // const data = useContext(TodoContext);
  // console.log(data);
  const {onCreate} = useContext(TodoContext);
  const [content, setContent] = useState("");
  const contentRef = useRef();

  const onChangeContent = (e) => {
    setContent(e.target.value);
  }

  const onKeyDown = (e) => {
    if (e.keyCode === 13) {
      onSubmit();
    }
  }

  const onSubmit = () => {
    if (content === "") {
        contentRef.current.focus();
        return;
    }
    onCreate(content);
    setContent("");
  }

  return (
    <div className='Editor'>
        <input ref={contentRef} value={content} onKeyDown={onKeyDown} onChange={onChangeContent} placeholder='새로운 Todo...' />
        <button onClick={onSubmit}>추가</button>
    </div>
  );
};

export default Editor;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// List.jsx
import './css/List.css';
import TodoItem from "./TodoItem";
import { useState, useMemo, useContext } from "react"; 
import { TodoContext } from "../App";

const List = () => {
  const { todos } = useContext(TodoContext);
  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 filteredData = getFilteredData();

  const {totalCount, doneCount, notDoneCount} = useMemo(() => {
    console.log("getAnalyzedData 호출");
    const totalCount = todos.length;
    const doneCount = todos.filter(todo => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;

    return {
      totalCount
      , doneCount
      , notDoneCount
    }
  }, [todos]);
  // 첫번째 인수 : 콜백함수
  // 두번째 인수 : 의존성배열 - deps

  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'>
            {filteredData.map((todo) => {
                return <TodoItem key={todo.id} {...todo}/>;
            })}
        </div>
    </div>
  );
};

export default List;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// TodoItem.jsx
import './css/TodoItem.css';
import { memo, useContext } from "react";
import { TodoContext } from "../App";

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

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

    return (
        <div className='TodoItem'>
            <input onChange={onChaneCheckbox} readOnly checked={isDone} type="checkbox" />
            <div className='content'>{content}</div>
            <div className='date'>{new Date(date).toLocaleDateString()}</div>
            <button onClick={onClickButton}>삭제</button>
        </div>
    );
}

export default memo(TodoItem);
  • 위와 같이 Context를 적용했더니 TodoItem 촤적화를 했음에도 불구하고 하나의 아이템만 수정해도 전체 아이템이 리렌더링된다.
  1. 최적화 풀린 이유
    • todos state가 변경되어 앱 컴포넌트가 리렌더링되면서 TodoContext.Provider의 value props로 전달하고있는 객체 자체가 새로 생성되었기 때문.
    • 이 문제는 context를 분리하여 해결할 수 있다.

Image

Image

This post is licensed under CC BY 4.0 by the author.