programming language/react

[React] React Hook - useContext, useRef, useMemo 적용하기

공대키메라 2022. 6. 10. 00:26

저번 시간에는 React의 Props와 State에 대해 알아보았고, 

 

React Hook의 기능 중 useState, useEffect를 알아보았다. (궁금하면 클릭!)

 

이번 시간에는 React Hook에 어떤 기능이 있는지 좀 더 알아볼 것이다.

 

이 내용은 다음 사이트를 참고했다.

 

참고

https://ko.reactjs.org/docs/hooks-intro.html

https://ko.reactjs.org/docs/hooks-reference.html

https://www.w3schools.com/react/react_usecontext.asp

https://www.daleseo.com/react-hooks-use-ref/

https://ko.reactjs.org/docs/hooks-reference.html#usememo

https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98

https://chanhuiseok.github.io/posts/react-7/

https://itprogramming119.tistory.com/entry/React-useMemo-%EC%82%AC%EC%9A%A9%EB%B2%95-%EB%B0%8F-%EC%98%88%EC%A0%9C


1.  Hook 간단 개요 

hook의 종류로는 여러가지가 있다.

 

https://ko.reactjs.org/docs/hooks-reference.html#usecontext

 

저번 시간에 간단하게 useState, useEffect를 어떻게 사용하는지 알아보았다.

 

이번에는 useContext, useMemo, useRef를 알아보고 적용할 것이다.

 

궁금한 분들은 위의 링크를 참고하길 바란다.

 

사실 hook 사용 설명은 React 사이트를 참고해서 보면 도대체 무슨 말인지 말아먹을 수 없는 것들이 있었다.

 

그러므로 여러 곳에서 정보를 찾아서 학습하길 추천한다. 

2. useContext 사용하기 

useContext Hook은 state 전역적으로 관리하기 위한 방법이다. 

 

이게 왜 필요하냐고? 다음 예시를 보자.

 

코드는 지난 시간에 이어서 동일한 곳에서 작성하도록 하겠다.

App.js 수정 - Header에 text props 추가

import "./App.css";
import Header from "./Header";

function App() {
  let name = "키메라 끼에ㅔ엥ㄱ";
  return (
    <div className="App">
      <Header text={"text"} name={"thelovemsg"} result={"result"} />
      <header className="App-header">hello React! with... {name}</header>
    </div>
  );
}

export default App;

Header.js

import Clock from "./Clock";

const Header = (datas) => {
  return (
    <div>
      <div>
        <h1>Hello, world!</h1>
        <h2>
          <Clock result={datas.result} />
        </h2>
      </div>
      header : {datas.text} & {datas.name}
    </div>
  );
};

export default Header;

Clock.js

import { useEffect, useState } from "react";

function Clock(props) {
  const [time, setTime] = useState(new Date());
  const [result, setResult] = useState(props.result);

  useEffect(() => {
    const id = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div>
      <h2>{time.toLocaleTimeString()}</h2>
      <h2>{result}</h2>
    </div>
  );
}

export default Clock;

 

필자는 저번 시간에는 없었던 시간초 세기를 하나의 component로 분리해서  clock.js로 작성하였다. 

 

이번에는 최상단 부모인 App에서 Clock으로 props를 이용해 result 를 전달하고 싶었다.

 

그래서 그 중간에 있는 Header.js의 경우에는 App 에서 전달해준 우리 result를 데이터를 꺼내서 다시 Clock으로 넘겨주는 역할을 한다.

 

여기서 무언가 쎄하다.

 

그렇다는 말은 부모 컴포넌트에서 어떤 것을 전달하려고 한다면 일일이 최종 목적지에 도달할 때 까지 전~부 위와 같은 방식으로 넘겨줘야 한다는 말이다. 이게 가당키나 한가?(앙!?!?)

 

Header.js 는 result 값을 사용하지 않더라고 전달하기 위해 일일이 적어주는 번거로움이 생기는 것이다. 

 

이를 해결하기 위한 것이 useContext이다. 

 

우리가 Context(문맥)으로 지정한 하위 컴포넌트 들에서는 Context로 설정해 준 데이터에 언제든지 접근이 가능하다. 

 

다음 코드를 본다면 더 이해가 잘 될 것이다.

App.js - Context 선언

import React, { createContext } from "react";
import "./App.css";
import Header from "./Header";

export const UserContext = React.createContext();
function App() {
  let name = "키메라 끼에ㅔ엥ㄱ";
  return (
    <UserContext.Provider value={"result"}>
      <div className="App">
        <Header text={"text"} name={"thelovemsg"} result={"result"} />
        <header className="App-header">hello React! with... {name}</header>
      </div>
    </UserContext.Provider>
  );
}

export default App;

React.createContext()로 context를 만들겠다고 선언한다. 그리고 export를 하는데 이유는 다른 곳에서 사용하기 위해서 그렇다. 이렇게 생성도니 UserContext를 필자는 Clock.js에서 사용할 것이다.

Header.js - props로 result값은 넘겨주지 않는다.

import Clock from "./Clock";

const Header = (datas) => {
  return (
    <div>
      <div>
        <h1>Hello, world!</h1>
        <h2>
          <Clock />
        </h2>
      </div>
      header : {datas.text} & {datas.name}
    </div>
  );
};

export default Header;

Context적용 전의 코드와 후의 코드를 보면 result Header.js에서 다시 꺼내서 넘겨줄 필요가 없다. 

 

그러면 Clock.js에서 바로 꺼내서 사용할 수 있는지 확인해 보겠다.

Clock.js - useContext로 result값 꺼내기

import { createContext, useContext, useEffect, useState } from "react";
import { UserContext } from "./App";

function Clock(props) {
  const [time, setTime] = useState(new Date());
  const result = useContext(UserContext);

  useEffect(() => {
    const id = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div>
      <h2>{time.toLocaleTimeString()}</h2>
      <h2>{result}</h2>
    </div>
  );
}

export default Clock;

 

우리가 App.js에서 선언한 Context인 UserContext를 React Hook인 useContext()를 이용해서 꺼내고 있다. 

 

export 할 때 선언한 이름 그대로 useContext에 parameter로 넘겨서 사용하면 된다. 

 

여기서 React가 사용해보면서 잘 만들었다고 생각한게 굉장히 hook의 이름들도 직관적이다.

 

state가 있다? state를 사용할래요! => useState
effect가 있다? effect를 사용할래요! => useEffect
context가 있다? context를 사용할래요! => useContext

 

이게 생각해보면 Context라는 것도 문맥으로 해석될 수 있는데 하나의 문맥 속에서는 데이터에 마음껏 접근할 수 있다는 것을 의미하고자 context라고 이름을 지은 게 아닌가 생각이 든다. 

 

어쨋든 다음과 같이 코드를 작성하면 화면은 다음처럼 나온다. 

 

 

useContext 이해 성공!

3. useRef 사용하기

useRef 훅은 renders들 사이에 값을 유지하도록 돕는다.

 

update될 때 다시 렌더링이 일어나지 않으면 변할 수 있는 값을 저장하는 데 사용한다. 

 

또한, DOM element에 접근하는데 사용되기도 한다.

 

DOM element에 접근하기 위해서는 useRef() 를 사용하여 Ref 객체를 만들고, 이 객체를 우리가 선택하고 싶은 DOM 에 ref 값으로 설정해 줘야 한다.

 

그러면, Ref 객체의 .current 값은 우리가 원하는 DOM 을 가르키게 된다.

 

Header.js - Count component 추가

import Clock from "./Clock";
import Count from "./Count";

const Header = (datas) => {
  return (
    <div>
      <div>
        <h1>Hello, world!</h1>
        <h2>
          <Clock />
          <Count />
        </h2>
      </div>
      header : {datas.text} & {datas.name}
    </div>
  );
};

export default Header;

Count.js

import { useEffect, useRef, useState } from "react";

const Count = () => {
  const [count, setCount] = useState(0);

  const countRef = useRef(0);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      couting star! : {count}
    </button>
  );
};

export default Count;

 

 

화면은 다음과 같이 나온다. 

 

4. useMemo 사용하기

React사이트를 보면 useMemo의 경우 메모이제이션 된 값을 반환한다고 설명한다. 

 

여기서  메모이제이션(memoization)이 무엇인지 알아야 한다. 

 

그래서 React에서 url로 걸어놓은 주소를 클릭해 보았는데...

 

 

이런 된장! React 분들 위키피디아로 이어주면 정성이 너무 없는거 아니오?

 

https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98

 

그런데 생각보다 굉장히 이해가 잘되게끔 설명되어 있다.

 

메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. 

동적 계획법의 핵심이 되는 기술이다. 메모이제이션 이라고도 한다.

 

메모이제이션은 이전 계산한 값을 메모리에 저장한다는 말이다.

 

useMemo는 React에서 성능 최적화를 위해서 사용하는데 useMemo를 이용하면 의존성이 변경되었을 때만 일부분의 값만 다시 계산하겠다는 말이다. 

 

rendering 작업은 고비용의 작업인데 이것을 최적화 하겠다는 것이 useMemo의 목적이다. 

 

useMemo를 사용하게 되면 흐름이 다음을 거친다.

 

Rendering => Component함수 호출 => Memoize된 함수 재사용

 

React 문서에는 예시 코드를 자세히 제공하지는 않는다.

 

하지만 걱정마라. 키메라가 한 번 예시를 작성하도록 하겠다.

 

 그전에 어떤 것이 성능적으로 최적화가 이루어지지 않았는지 쉽게 알기 위해 chrome에서 extention을 하나 다운받을 것이다.

 

 

React Developer Tools 인데 말 그대로 리액트 개발자 툴이다. 

 

필자는 이미 다운을 받아서 버튼이 삭제 표시로 나온다. 

 

다운을 받고... f12를 누르고 component를 눌러본다.

 

 

들어가서 설정 아이콘을 클릭하고 rendering 될 때 마다 하이라이팅이 되도록 체크해준다.

 

 

그러면 이제 시간이 지날 때 마다 rendering이 되는것을 확인할 수 있다.

 

 

짜잔!

 

Clock Component가 전체적으로 시간이 흐를 때 마다 계속 rendering 되고있음을 확인할 수 있다.

 

저기 연한 색깔의 상자 안이 새롭게 rendering된다는 것이다.

 

하여간 다시 useMemo를 사용하는 방법으로 돌아오겠다.

 

기존의 코드를 조금만 손보는 것에서 출발하겠다.

App.js

import React from "react";
import "./App.css";
import Header from "./Header";

export const UserContext = React.createContext();
function App() {
  let name = "키메라 끼에ㅔ엥ㄱ";
  let state = {
    result: "result",
    datas: ["test1", "test2", "test3"],
    text: "text",
    name: "thelovemg",
  };
  return (
    <UserContext.Provider value={state}>
      <div className="App">
        <Header />
        <header className="App-header">hello React! with... {name}</header>
      </div>
    </UserContext.Provider>
  );
}

export default App;

Header.js

import { useContext } from "react";
import { UserContext } from "./App";
import Clock from "./Clock";
import Count from "./Count";

const Header = () => {
  const { text, name } = useContext(UserContext);
  return (
    <div>
      <div>
        <h1>Hello, world!</h1>
        <h2>
          <Clock />
          <Count />
        </h2>
      </div>
      header : {text} & {name}
    </div>
  );
};

export default Header;

Clock.js

import { createContext, useContext, useEffect, useRef, useState } from "react";
import { UserContext } from "./App";

function Clock(props) {
  const [time, setTime] = useState(new Date());
  const { result } = useContext(UserContext);

  useEffect(() => {
    const id = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => clearInterval(id);
  }, [time]);

  return (
    <div>
      <h2>{time.toLocaleTimeString()}</h2>
      <h2>{result}</h2>
    </div>
  );
}

export default Clock;

 

자, 기존의 코드를 다음처럼 수정을 했다.

 

별도로 넘겨주던 data들을 전부 context로 집어넣고 꺼내 쓰는 방식으로 통일해버렸다.

 

이번에는 새로운 component인 DataList라는 component를 생성해서 반복문을 이용해서 화면에 출력할 것이다. 

 

점점 코드가 길어지기에 스압 주의 부탁한다.

App.js

import React from "react";
import "./App.css";
import Header from "./Header";

export const UserContext = React.createContext();
function App() {
  let name = "키메라 끼에ㅔ엥ㄱ";
  let state = {
    result: "result",
    text: "text",
    name: "thelovemg",
  };
  return (
    <UserContext.Provider value={state}>
      <div className="App">
        <Header />
        <header className="App-header">hello React! with... {name}</header>
      </div>
    </UserContext.Provider>
  );
}

export default App;

Header.js

import { useContext, useEffect, useRef, useState } from "react";
import { UserContext } from "./App";
import Clock from "./Clock";
import Count from "./Count";
import DataList from "./DataList";

const Header = () => {
  const [datas, setDatas] = useState([
    {
      name: "test1",
      seq: 123,
    },
    {
      name: "test2",
      seq: 122,
    },
    {
      name: "test4",
      seq: 124,
    },
  ]);

  const { text, name } = useContext(UserContext);

  const [resultStr, setResultStr] = useState("");
  const addTagContent = useRef("");

  useEffect(() => {
    addTagContent.current = resultStr;
  }, [resultStr]);

  return (
    <div>
      <div>
        <h1>{resultStr}</h1>
        <button
          onClick={() => {
            setResultStr((c) => c + "test");
          }}
        >
          add string to first tag... :
        </button>
        <h2>
          <Clock />
          <Count />
        </h2>
        <DataList datas={datas} />
      </div>
      <br />
      header : {text} & {name}
    </div>
  );
};

export default Header;

DataList.js

import DataItem from "./DataItem";

const DataList = ({ datas }) => {
  const setDatasInSeq = (datas) => {
    console.log("dataList 출력하기");
    datas.sort((a, b) => a.seq - b.seq);
    return [...datas];
  };
  return (
    <div>
      {setDatasInSeq(datas).map((test, idx) => (
        <DataItem key={idx} {...test} />
      ))}
    </div>
  );
};

export default DataList;

DataItem.js

const DataItem = ({ name, seq }) => {
  return (
    <div>
      {name} and {seq}
    </div>
  );
};

export default DataItem;

 

출력한 화면은 다음과 같다.

 

따단~!

버튼을 클릭하면 버튼 위 tag에 글이 추가되도록 했다.

여기서 문제는 'dataList 출력하기' 가 콘솔창에 버튼 클릭 때 마다 계속 발생한다는 것이다. 

 

오잉? 나는 버튼만 눌러서 text만 추가하려고 했는데 다른 곳이 왜 re-rendering이 일어나는건지?

 

여러번 클릭한 결과 역시 같은 결과를 보여준다.

 

---------------------------

저기 test2 and 123 부분이 새롭게 highliting 되야한다. 사진은 양해 부탁...ㅠㅠ 

--------------------------

 

 

이 문제의 이유는 Header.js에 있는데 

 

다시 한번 Header.js를 보겠다.

Header.js

import { useContext, useEffect, useRef, useState } from "react";
import { UserContext } from "./App";
import Clock from "./Clock";
import Count from "./Count";
import DataList from "./DataList";

const Header = () => {
  const [datas, setDatas] = useState([
    {
      name: "test1",
      seq: 123,
    },
    {
      name: "test2",
      seq: 122,
    },
    {
      name: "test4",
      seq: 124,
    },
  ]);

  const { text, name } = useContext(UserContext);

  const [resultStr, setResultStr] = useState("");
  const addTagContent = useRef("");

  useEffect(() => {
    addTagContent.current = resultStr;
  }, [resultStr]);

  return (
    <div>
      <div>
        <h1>{resultStr}</h1>
        <button
          onClick={() => {
            setResultStr((c) => c + "test");
          }}
        >
          add string to first tag... :
        </button>
        <h2>
          <Clock />
          <Count />
        </h2>
        <DataList datas={datas} />
      </div>
      <br />
      header : {text} & {name}
    </div>
  );
};

export default Header;

 

resultStr이 새롭게 변할 때 마다 모든 return 값 안에 있는 것들이 re-rendering되고 있다는 말이다!

 

re-rendering이 일어날 때 마다 DataList내에 setDatasInSeq도 다시 생성되서 동일한 작업을 하기 때문에 dataList 출력하기 라는 문구가 계속 뜨는 것이다. 

 

이러한 불필요한 작업을 막도록 도와주는 친구가 우리의 useMemo이다.

 

그러면 DataList.js에 이렇게 적용해보도록 하겠다.

 DataList.js

import { useMemo } from "react";
import DataItem from "./DataItem";

const DataList = ({ datas }) => {
  const setDatasInSeq = useMemo(() => {
    console.log("datas :: ", datas);
    console.log("dataList 출력하기");
    return [...datas];
  }, [datas]);

  return (
    <div>
      {setDatasInSeq(datas).map((test, idx) => (
        <DataItem key={idx} {...test} />
      ))}
    </div>
  );
};

export default DataList;

 

그런데 이렇게 하면 오류가 생긴다.

 

setDataInSeq를 useMemo로 감싸게 되면 더이상 함수가 아니게 된다.

 

즉, useMemo로 감싸게 되면 결과값을 반환해주게 된다!

 

그 결과값이 현재 setDatasInSeq에 담겨있는 것이다. 

 

그러면 다음과 같이 작성하면 된다.

DataList.js - useMemo 적용 완료

import { useMemo } from "react";
import DataItem from "./DataItem";

const DataList = ({ datas }) => {
  const setDatasInSeq = useMemo(() => {
    console.log("혹시?");
    datas.sort((a, b) => a.seq - b.seq);
    return [...datas];
  }, [datas]);

  return (
    <div>
      {setDatasInSeq.map((test, idx) => (
        <DataItem key={idx} {...test} />
      ))}
    </div>
  );
};

export default DataList;

 

여기서 주의점은 useMemo의 뒤에 어떤 값을 기준으로 우리가 전달한 callback 함수가 다시 실행될지를 선택하면 된다. 

 

필자는 props로 전달받은 datas를 기준으로 변경사항이 있다면 setDatasInSeq를 작동시켜 정렬 후에 출력할 것이다. 

 

그러면 여러번을 클릭해도 메모이제이션 기능을 지원하는 우리 useMemo 덕분에 함수가 다시 실행되지 않는다!

 

즉, 다시 re-rendering이 전반적으로 되더라도 기능 자체를 기억하는 기능인 useMemo를 사용해서 최적화를 한 것이다


이렇게 긴 오늘의 대장정을 끝마치겠다.

 

사실 어제부터 글을 정리했는데

 

필자도 제대로 이해하고 예시를 어떻게 적용할 수 있는지 공부하다보니 글이 너무 길어지고 말았다. 

 

그럼 안녕...