React 라이브러리

13. Context API

문정훈 2021. 12. 26. 14:40

1. Context API란

hooks로 리액트 컴포넌트를 만들 때 state들이 많아지면 그 state들을 하나의 묶음으로 관리하기 위해 userReducer를 사용하여 state들을 관리하는 방법을 이전에 정리하였다.

이전 장에서 Tictactoe를 만들 때 상위 컴포넌트에서 하위 컴포넌트를 만들고 하위 컴포넌트에서 상위 컴포넌트의 state(useReducer로 관리 되는 state)들의 값에 접근(접근 또는 변경) 하기 위해 상위 부모 컴포넌트의 dispatch 함수를 하위 컴포넌트로 전달해줘야했다. 

만약 하위 컴포넌트 상속 관계가 계속 된다면 상위 컴포넌트의 dispatch와 같은 프로퍼티(또는 함수)들을 수작업으로 

하위 컴포넌트들에게 계속 전달해줘야하는 불편함이 있었다.

이것을 해결해주는 방법으로 Context API를 사용하는 것이다. 

 

8장에서 지뢰찾기를 만들면서 useReducer로 state들을 관리하고 Context API기법을 적용하여 지뢰찾기 리액트 버전을 만든다. 

 

1) 부모 컴포넌트에서 createContext로 지정하기

const React = require('react', createContext);
const { Component } = React;

export const TableContext = createContext({
  tableData: [],
  dispatch : ()=> {},
});

const initialState = {
  tableData = [],
  result ='',
}

const reducer = (state, action) => {
  switch(action.type) {
    default:
    return state;
  }
}
const MineHunt = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
   <>
    <TableContext.Provider value = {{tableData: state.tableData, dispatch}}>
      <Form/>
      <div>{state.timer}</div>
      <Table/>
      <div>{state.result}</div>
    </TableContext.Provider>
   </>
  )
};

module.exports = MineHunt;

 

Context API를 부모 컴포넌트에서 선언한다 createContext라는 함수가 있고 이 함수를 부모 컴포넌트에서 우선 선언한다.

위 예시와 같이 TableContext 라는 프로퍼티를 만들고 그 안에 createContext() 함수를 호출한다. 

그리고 그 부모 컴포넌트에서 호출하는 자식 컴포넌트를 createContext가 제공하는 Privider로 감싸줘야한다.

위와 같이 감싸주게 되면 자식 컴포넌트들은 부모 컴포넌트의 state뿐만 아니라 프로퍼티등을 전달에 전달해야하는 상황없이 자식, 손자.. 컴포넌트 모두 바로 접근할 수 있게 된다. 

  1. 부모 컴포넌트에서 createContext API를 지정하고 이 함수를 호출하는데 매개변수로 기본 값인 객체를 지정해줘야한다. 기본값으로는 value에서 지정된 객체에서 사용되는 프로퍼티의 기본 형태를 적어주는것으로 충분함(기본 값은 딱히 큰 의미가 없음)
  2. 부모 컴포넌트에서 호출하는 자식 컴포넌트들을 Provider안에 지정해준다.
  3. Provider 태그의 props인 value값에 객체를 지정하는데 객체의 프로퍼티로 모든 자식 컴포넌트에서 사용할 부모 프로퍼티(state 등)을 적어주면 된다.

※ TableContext 프로퍼티는 자식에서 useContext의 매개변수로 가져오기 때문에 export로 접두어를 붙이고 자식 컴포넌트에서 require(또는 import)로 가져올 수 있게 해준다. 

 

 

2) 자식 컴포넌트에서 createContext로 전달 받은 값들 사용하기 

const React = require('react');
const {useState, useCallback, useContext} = React;
const MineHunt = require('./MineHunt.jsx');
const {TableContext} = MineHunt;


const onChangeRow = useCallback((e) => {
  setRow(e.target.value);
}, []);

const onChangeCell = useCallback((e) => {
  setCell(e.target.value);
},[]);

const onChangeMine = useCallback((e) => {
  setMine(e.target.value)
},[]);

const onClickBtn = useCallback((e)=>{


}, []);


const Form = () => {
  const [row, setRow] = useState(10);
  const [cell, setCell] = useState(10);
  const [mine, setMine] = useState(20);
  const value = useContext(TableContext);  
  //const {tableData, dispatch } = useContext(TableContext);  

  return(
    <div>
      <input type="number" placeholder="세로" value = {row} onChance = {onChangeRow}/>
      <input type="number" placeholder="가로" value = {cell} onChance = {onChangeCell}/>
      <input type="number" placeholder="지뢰" value = {mine} onChance = {onChangeMine}/>
      <button onClick = {onClickBtn}>시작</button>
    </div>
  )
}

module.exports = Form;

 

자식 컴포넌트에서는 위와 같이 우선 useContext를 선언한 뒤

변수 value 의 값으로 useContext(TableContext); 이렇게 함수와 인자를 지정하여 부모 컴포넌트에서 전달 한 value의 객체를 자식으로 전달하게 된다. 

useContext(TableContext)의 리턴 값은 부모에서 지정한 value의 객체이다.

따라서 주석문과 같이 구조 분해 형태로 값을 지정할 수 있다. 

 

 

3) Context API의 성능 문제 발생과 해결

const React = require('react', createContext);
const { Component } = React;

export const TableContext = createContext({
  tableData: [],
  dispatch : ()=> {},
});

const initialState = {
  tableData = [],
  result ='',
}

const reducer = (state, action) => {
  switch(action.type) {
    default:
    return state;
  }
}
const MineHunt = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const value = useMemo(()=>({tableData: state.tableData, dispatch}), [state.tabledata]);
  return (
   <>
    <TableContext.Provider value ={value}>
      <Form/>
      <div>{state.timer}</div>
      <Table/>
      <div>{state.result}</div>
    </TableContext.Provider>
   </>
  )
};

module.exports = MineHunt;

성능 최적화하기가 어렵다. 기본적인 문제중 하나는 우선 위와같이 부모 컴포넌트에서 value를 위와 같이 지정하였는데 위와 같은 형태로 지정하는게 성능 최적화를 어렵게 만든다. 

그 이유는 MineHunt 가 새로 리랜더링 될 때마다
<TableContext.Provider value = {{tableData: state.tableData, dispatch}}>이 부분의 value안의 객체들도 새로 생성된다. 

그러면 Context API를 사용하는 자식들도 새로 리랜더링되게 된다. 

따라서 위 코드와 같이 캐싱을 한번 하여 수정한다.

즉 useMemo를 사용하여 value를 기억하게 하여 부모 컴포넌트(MIneHunt)의 리랜더링 때 value의 값이 변경되지 않도록하여 <TableContext.Provider> 컴포넌트의 props( value값)이 변경되지 않도록하여 재랜더링을 하지 않게하는 것이다.

 

※ 헷갈릴거 정리

부모 컴포넌트에서 지정한 하위 컴포넌트가 있다. 부모 컴포넌트가 재랜더링이 되면 그 아래 자식 컴포넌트들은 랜더링이 다시 되는 조건이 자식 컴포넌트의 state값이 변경되거나 props의 값이 변경된다면 자식 컴포넌트 역시 부모 컴포넌트의 리랜더링에 맞춰 랜더링을 다시한다. 따라서 자식 컴포넌트를 pureComponent를 사용하여 재랜더링이 어떤 조건하에 이뤄지도록 지정하여 부모 컴포넌트가 재런더링이 되도 자식 컴포넌트가 재랜더링이 되지 않도록 막았었다. 

 

TableContext.Provider역시 컴포넌트이고 이는 기본적으로 PureComponent로 지정되어 있다고 가정하면 이해가 빠름

value의 값이 변경되지 않도록 지정하면 즉 TableContext.Provider 컴포넌트의 props의 값을 변경하지 않으므로 랜더링을 다시 하지 않는다. 따라서 TableContext.Provider의 하위 자식들도 재랜더링 하지 않게 됨.

 

1)~3)과정으로 자식 컴포넌트에서는 자유롭게 부모 컴포넌트의 프로퍼티를 사용할 수 있게 되었다.

MineHunt에서 Context API를 지정하였고 그 자식 컴포넌트인 Form컴포넌트에서는 dispatch함수를 바로 사용할 수 있게 되었음.

value.dispatch이렇게 사용하거나 구조분해 할당하였다면 dispatch로 바로 사용할 수 있다.