React 61장 - React Context (2) 여러 개의 context
포스트
취소

React 61장 - React Context (2) 여러 개의 context

React

Context API

사용하다보니 메모

  • Context를 사용하다보면 하나의 상태값을 여러 컴포넌트에서 사용하기 때문에, 상태값을 사용하고 있는 여러 컴포넌트에서 불필요한 리렌더가 발생할 수 있다고 한다.
  • 예를 들어, example이라는 값을 const {example} = useContext(ExampleContext)로 두 개의 컴포넌트에서 사용하고 있다.
  • A 컴포넌트에서 값을 변경했어도, 이 값을 사용하고 있는 B 컴포넌트에서도 리렌더링이 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type DefaultValue = {
  example: string;
  setExample: React.Dispatch<React.SetStateAction<string>>;
};

const AppContext = createContext<DefaultValue>({
  example: "",
  setExample: () => {},
});

function App() {
  const [example, setExample] = useState("");

  return (
    <AppContext.Provider value=>
      <ExampleHeader />
      <ExampleBody />
    </AppContext.Provider>
  );
}
  • 위와 같은 코드에서 ExampleBody 컴포넌트에서 example값을 변경한다고 가정하면, 최상위 컴포넌트에 포함돼있는 ExampleHeader에서도 리렌더링이 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function ExampleHeader(){
    console.log(1);

    return(
        ...
    )
};

function ExampleBody(){
    const {setExample} = useContext(AppContext);

    // example을 변경하는 핸들러
    function changeExampleHandler(e:React.EventHandler){
        setExample(e.target.value);
    }
    return (
        // example 변경
    )
};
  • example값이 변경될 때마다 console에 1이 찍히며, 불필요한 리렌더링을 해결할 필요가 있었다.

방법 1. React.memo 사용하기

참고자료 React.memo 참고자료 - 리액트 memo를 사용하기 전에

  • 컴포넌트를 렌더링 할 때 사용하는 React.memo를 사용한다.
  • memo는 컴포넌트를 렌더링한 뒤, 이전 렌더링 결과와 다르면 업데이트를 하는데, Context를 사용하여 값을 변경한다 해도, 이를 사용하는 컴포넌트가 아닌 이상 컴포넌트의 변경점은 없다.
  • 따라서 불필요한 리렌더링이 발생하지 않는다.
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
type ExampleType = {
  example: string,
  setExample: React.Dispatch<React.SetStateAction<string>>,
};

const ExampleContext = createContext({
  example: "",
  setExample: () => {},
});

const App = () => {
  const [example, setExample] = useState();

  return (
    <Example.Provider value=>
      <ChildOne />
      <ChildTwo />
    </Example.Provider>
  );
};

const ChildOne = () => {
  console.log("ChildOne component render");
  return null;
};

const ChildTwo = () => {
  console.log("ChildTwo component render");
  const { example, setExample } = useContext(ExampleContext);

  return <button onClick={() => setExample("changed")}>example 변경</button>;
};
  • 위와 같은 코드가 있다면 콘솔에 컴포넌트 1,2에 해당하는 콘솔 메시지가 전부 찍혀있을 것이다.
  • ChildTwo에서 버튼을 사용하여 example값을 변경할 때, 값을 사용하고 있지 않은 ChildOne 컴포넌트에 대한 불필요한 리렌더링이 발생하기 때문에 콘솔 메시지가 찍히는 것을 볼 수 있다.
  • 이는 컴포넌트의 상태가 변경되면 해당 컴포넌트의 하위 컴포넌트도 리렌더링 되는 컴포넌트의 특성때문이다.
  • ChildOneexample의 상태값을 전혀 사용하고 있지 않지만, App컴포넌트의 하위 컴포넌트에 속하기 때문에, 설령 값을 사용하지 않고, ChildTwo컴포넌트에서 값을 변경하더라도 ChildOne컴포넌트에 대한 불필요한 리렌더링이 발생한다.
  • 이를 방지하기 위해 React.memo를 사용하여 컴포넌트의 리렌더링을 방지할 수 있다.
const ChildOne = React.memo(() => {
  console.log("ChildOne component render")
  return null;
});
  • ChildOne 컴포넌트를 React.memo로 감싸 리렌더링을 방지할 수 있는데, React.memo는 순수함수를 상속받는 것과 같이 동작한다고 한다.
  • 처음 렌더링 될 때 콘솔에 "ChildOne component render"가 찍히고, ChildTwo에서 상태값을 변경하더라도 ChildOne 컴포넌트에 대한 변경점이 없기 때문에 리렌더링이 발생하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const ChildTwo = () => {
  const { example, setExample } = useContext(ExampleContext);
  console.log("ChildTwo component render");

  return (
    <React.Fragment>
      <ChildThree />
      <button onClick={() => setExample("changed")}>example 변경</button>
    </React.Fragment>
  );
};

const ChildThree = () => {
  console.log("ChildThree component render");

  return null;
};
  • 만약 ChildTwo 컴포넌트의 하위 컴포는트로 ChildThree 컴포넌트가 있다면 어떨까.
  • ChildTwo에서 상태값을 변경할 때, 상태값을 사용하고 있지 않은 ChildThree 컴포넌트에 대한 리렌더링도 똑같이 발생한다.
  • 이는 위에서 설명한 컴포넌트의 특성때문에 ChildTwo 컴포넌트가 리렌더링 되면서 하위 컴포넌트들 또한 리렌더링 시키기 때문이다.
1
2
3
const ChildThree = React.memo(() => {
  ...
})
  • 똑같이 React.memo를 사용하여 리렌더링을 방지할 수 있지만, 매 컴포넌트마다 React.memo를 기본값으로 작성해주는 것도 번거로운 일이다.
  • 리렌더링이 발생하는 컴포넌트에 대한 정리를 통해서 이러한 과정을 최소화 할 수 있을 거라 생각하지만, 더 좋은 방법이 있는지에 대해서도 생각해야겠다.

방법 2. 상태 끌어올리기

참고자료

  • Context를 분리하고 이를 최상위 컴포넌트인 App에서 관리하여 전체가 리렌더링 되는 것을 방지한다.
  • 수정 전의 예시와 수정 후의 예시는, 수정 전의 코드는 Context에 관한 상태값을 App컴포넌트에서 관리한다.
  • 그렇기에 수정 사항이 발생하면 하위 컴포넌트들이 전부 리렌더링된다.
    • 이진 트리에서 꼭대기의 시작점부터 변경되는 것이라고 생각하면 된다.
  • 수정 후의 예시는, App에서 ChildOne, ChildTwo를 렌더링하여 ContextProvider로 넘겨주는 역할을 하기 때문에 App의 리렌더링이 발생하지 않는 이상 ContextProvider가 리렌더링되어도 하위 컴포넌트들의 변경사항은 발생하지 않는다.
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
// 기존 코드
const AppContext = createContext();

const App = () => {
  const [str, setStr] = useState();
  console.log("App render")

  return (
    <AppContext.Provider value=>
      <ChildOne/>
      <ChildTwo/>
    </AppContext.Provider>
  )
};

// ChildOne
const ChildOne = () => {
  console.log("ChildOne render");

  return null
}

// ChildTwo
const ChildTwo = () => {
  const {str, setStr} = useContext(AppContext);

  console.log("ChildTwo render");

  return (
    <div>
      <span>current Str: {str}</span>
      <button onClick={() => setStr(...)}>Random Str</button>
    </div>
  )
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 수정 코드
const AppContext = createContext();

const ContextProvider = ({ children }) => {
  const [str, setStr] = useState();
  console.log("Provider render");

  return (
    <AppContext.Provider value=>
      {children}
    </AppContext.Provider>
  );
};

const App = () => {
  console.log("App render");

  return (
    <ContextProvider>
      <ChildOne />
      <ChildTwo />
    </ContextProvider>
  );
};

방법 3. Context 분리하여 사용하기

참고자료

  • 별도의 ContextProvider을 만들어 분리하고, App 컴포넌트에서 상태끌어올리기를 사용하는 방식을 함께 사용한다.
  • 빠르게 예시를 봐보자.
  • 아래와 같이 Context, Provider를 분리하여 작성하고, App 컴포넌트에서 reduce함수를 사용해 사용할 Context를 모두 가져와주고, 렌더링 될 App의 하위 컴포넌트들을 전부 가져온다.
  • 이렇게 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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// StrProvider.js
export const StrContext = createContext("set Str");

export const StrProvider = ({children}) => {
  const [str, setStr] = useState("");

  console.log("StrProvider render")

  return (
    <StrContext.Provider value=>
      {children}
    </StrContext.Provider>
  )
};

// NumProvider.js
export const NumContext = createContext(0);

export const NumProvider = ({children}) => {
  const [num, setNum] = useState(0);

  console.log("NumProvider render");

  return (
    <NumContext.Provider value=>
      {children}
    </NumContext.Provider>
  )
}

// App.js
const AppProvider = ({ contexts, children }) =>
  contexts.reduce(
    (prev, context) =>
      React.createElement(context, {
        children: prev,
      }),
    children
  );

const App = () => {
  console.log("App render");

  return (
    <AppProvider contexts={[StrProvider, NumProvider]}>
      <ChildOne />
      <ChildTwo />
    </AppProvider>
  )
};

// ChildOne
const ChildOne = () => {
  const {str, setStr} = useContext(StrContext);

  console.log("ChildOne render")

  return (
    <div>
      <span>current Str: {str}</span>
      <button onClick={() => setStr(...)}>random Str</button>
    </div>
  )
}


// ChildTwo
const ChildTwo = () => {
  const {num, setNum} = useContext(NumContext);

  console.log("ChildTwo render");

  return (
    <div>
      <span>current Num: {num}</span>
      <button onClick={() => setNum(...)}>random Num</button>
    </div>
  )
}

방법 4. 값을 별도의 객체로 관리 (PASS)

참고자료

  • 객체를 통째로 상태값으로 관리하게 되면, 상태 객체가 변경되지 않는 경우에는 하위의 컴포넌트가 불필요하게 리렌더링 되지 않는다고 한다.
  • 잘 이해를 못 하겠다…상태 객체는 어차피 변경될 여지로 만들어 두는 게 아닌가…? 어떻게 하면 리렌더링이 일어나지 않을까 여러 방법으로 시도해보았지만, 위에서 설명하는 리렌더링이 안 일어나는 방법은 뭔지를 모르겠다.
  • 예시 1번 코드를 예시 2번 코드로 바꿔서 얻는 이점이 뭘까…모르겠다.

예시 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const UserContext = createContext({ username: "unknown", age: 0 });

export default function App() {
  const [username, setUsername] = useState("");
  const [age, setAge] = useState(0);

  return (
    <div>
      <UserContext.Provider value={(username, age)}>
        <Profile />
      </UserContext.Provider>
    </div>
  );
}

예시 2

1
2
3
4
5
6
7
8
9
10
11
12
13
const UserContext = createContext({ username: "unknown", age: 0 });

export default function App() {
  const [user, setuser] = useState({ username: "horong", age: 23 });

  return (
    <div>
      <UserContext.Provider value={user}>
        <Profile />
      </UserContext.Provider>
    </div>
  );
}

방법 5. useMemo 사용하기 (PASS)

참고자료

  • useMemo를 활용하여 리렌더링을 방지하는 방법에 대해 설명하고 있다.
  • 직접 사용하여 실험해보고 있지만 리렌더링은 똑같이 발생한다.
  • 의문이 들었던 것은 useMemo로 값을 기억할 수 있지만, Context에서 사용하는 값이 변경될 때면 메모이제이션이 새로 발생하며 리렌더링이 당연히 일어나게 되는 것 아닐까? 이 방법으로 어떻게 리렌더링을 최적화하는 것인지 의문이다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.