Chap13. 리액트와 최적화 테크닉

Muru·2023년 11월 30일

[React] 지식 저장소

목록 보기
18/30
post-thumbnail

13.1 : Chap13에선 무엇을 배우는가?

  1. 리액트가 백그라운드에서 동작하는 원리.
  2. 가상 DOM과 DOM의 업데이트에대한 이해.
  3. 리액트가 관리하는 상태 객체의 갱신에 대한 이해.


13.2 : 리액트가 실제로 작동하는 방식

13.2.1 : 리액트의 역할

리액트가 하는 역할은 가상 DOM이라는 개념을 사용하는것이라고 할 수 있다.
이 가상 DOM은 앱이 마지막에 만들어내는 컴포넌트 트리를 결정한다.
각각의 하위 트리를 갖고 있는 컴포넌트들은 JSX 코드를 반환하고, 이러한 컴포넌트들이 모이고 모여서 가상 DOM으로 컴포넌트 트리의 현재 모양과 최종모양을 결정짓게 되는것이다.

가령, 상태가 업데이트 되었다고 가정해보면 이 상태 정보는 리액트 DOM으로 전달이 된뒤에, 갱신 전후 상태 차이를 인식하고 리액트가 컴포넌트 트리를 통해 구성한 가상 DOM과 일치하도록 실제 DOM을 조작한다.

흐름도를 번호를 매긴다면 다음과 같다.

  1. 상태 업데이트
  2. 리액트 DOM으로 전달
  3. 상태 업데이트 전과 상태 업데이트 후 차이를 인식
  4. 가상 DOM 형성
  5. 실제 DOM 조작

13.2.2 : 컴포넌트 함수의 재평가는 DOM 렌더링이 아니다.

헷갈릴 수 있는 개념적인 부분이다.
컴포넌트 함수가 재실행되어 리액트가 이를 재평가할때 DOM은 다시 렌더링 하는것이 아니다.

리액트에 의해서 컴포넌트 함수가 재실행된다고 해서 실제 DOM의 부분들이 다시 렌더링을 필수적으로 한다는것이 아니라는 의미이다.

컴포넌트 , 리액트 , 실제 DOM을 구분하여 헷갈리지말자.
컴포넌트는 상태와, props, Context가 변경될때 재평가가된다. 이때 리액트는 컴포넌트 함수를 다시 실행한다.
실제 DOM은 리액트가 구성한 컴포넌트 이전 상태와, 트리 , 현재 상태간의 차이점을 기반으로 변경이 필요할 때만 업데이트가 된다. 즉 실제 DOM은 필요한 경우에만 변경이된다.

이러한 작동방식은 성능적으로 최적화가 많이 된다.
가상 DOM으로 상태 변경 전과 상태 변경 후에 스냅샷 간의 차이점을 알아낸뒤.
차이점을 실제 DOM에 전달하는 구조를 갖는다는 의미이다.
즉 실제 DOM에서는 차이점이 있는 부분만 다시 렌더링이 되고 나머지 부분은 그대로인것이다.

13.2.3 : 컴포넌트 업데이트 실행중

그렇다면 실제 코드를 작성하여 좀더 와닿도록 해보자.

App.js

function App() {
  const [showParagraph, setShowParagraph] = useState(false);

  const toggleParagrapHandler = () => {
   setShowParagraph( prevShowParagraph => !prevShowParagraph);
  }
  return (
   <div className="app">
    <h1>Hi there!</h1>
    {showParagraph &&<p>this is new!</p>}
    <Button onClick={toggleParagrapHandler}>Toggle Paragraph</Button>
   </div>
  );
}

해당 "Toggle Paragraph“ 버튼을 누르면 새로운 문구가 나오게하는 코드이다.

버튼을 눌렀더니 "this is new!"라는 문구가 나왔다.

App.js 컴포넌트 함수에 코드를 하나 더 집어넣고 버튼을 여러번 클릭해보자.

console.log("앱 실행중“);

첫번째 “앱 실행중“은 최초 컴포넌트 함수가 실행되었을때 콘솔을 보여준다.

6번이나 출력된 “앱 실행중”은 6번의 버튼클릭으로 6번의 컴포넌트 함수를 재평가하여 실행되었음을 알 수 있다.


이렇게 우리는 상태가 변경이되면 컴포넌트의 전체가 재실행이되고 재평가된다는 사실을 확인하였다.
그렇다면 실제 DOM에는 어떠한 영향이 있을까??
HTML - Elements 에서 DOM 변경 부분을 볼 수 있다. 버튼을 눌러서 변경점을 확인해보자.

<p>this is new!</p> 부분이 반짝이는것을 확인할 수 있는데, 해당 부분이 DOM에서 바뀌었다고 알려주는 부분이다.
바뀐 <p> 단락을 제외하고는 나머지는 반짝이지 않는다.
이점이 정말 중요한 부분이다.
가장 최근 스냅샷간의 차이만 다시 렌더링 시키고 나머지는 그대로인것이다.

13.2.4 : 자식 컴포넌트 재평가 자세히 살펴보기

위에서 썻던 코드를 분리해보자.

App.js

function App() {
  const [showParagraph, setShowParagraph] = useState(false);

  console.log('앱 실행중');

  const toggleParagrapHandler = () => {
   setShowParagraph( prevShowParagraph => !prevShowParagraph);
  }
  return (
   <div className="app">
    <h1>Hi there!</h1>
    <DemoOutput show={showParagraph}/>
    <Button onClick={toggleParagrapHandler}>Toggle Paragraph</Button>
   </div>
  );
}
export default App;

DemoOutput.js

import React from 'react';
const DemoOutput = (props) => {
   console.log('데모 실행중');
   return <p>{props.show ? 'This is new!' : ''}</p>;
};

export default DemoOutput;

App.js에서 showParagraph라는 불리언 상태를 Button에 의해 바뀌는데, 해당 불리언 상태를 DemoOutput 컴포넌트에 show 라는 props로 전달한다.
DemoOutput컴포넌트는 App.js에서 보내준 props의 상태를 의존하여 단락을 표시할지 안할지 결정이된다.

궁금증1 : App.js는 함수 재평가가 실행되지 않을것인가?

생각해볼때, 버튼을 클릭하게되면 최종 출력을 담당하는 DemoOutput 컴포넌트만 함수가 재평가 실행이되고 App.js는 재평가가 실행되지 않는다고 생각 할 수 있다. 하지만 아래 사진을 보자.

버튼을 클릭할때마다 app.js에 있는 콘솔로그 코드인 “앱실행중” 과
DemoOutput.js에 있는 콘솔로드 코드인 “데모 실행중”이 출력되는것을 볼 수 있다.
실제 변경은 DemoOutput.js 에서 하므로 당연히 “데모 실행중”이 출력되는것은 직관적으로 알 수 있지만
상태 관리를 하고 있는 App.js 역시 컴포넌트 함수가 재평가가 되어 “앱 실행중”이 출력된다는것을 알아두자.


궁금증2 : App.js 에서 DemoOutput.js 에게 props 전달하는것이 고정된다면 DemoOutput.js 는 재평가가 되지 않을것인가?

단지 단순하게 생각해볼때, App.js에서 하위 컴포넌트에게 props로 전달하는것이 고정된다면 DOM이 바뀔 여지가 없기 때문에 하위 컴포넌트인 DemoOutput컴포넌트 함수는 재평가 될 필요가 없는것이 아닌가 라는 궁금증이 생기는것이다.

App.js 코드를 수정해보자. props로 boolean값을 고정시킨채로 전달한다.
App.js

<DemoOutput show={false}/>

여기서 Button을 누른 결과로, 그대로 자식 컴포넌트인 DemoOutput.js가 재평가되어 실행되었음을 알 수 있다.

왜 이런 결과가 나오는것일까? Step by Step으로 살펴보자.
1. App 함수는 상태가 변경되었기 떄문에 재실행되는것은 자명하다. (showParagraph)

APP 함수 부분에는 반환문(return)이 있고 이 반환문에는 JSX 코드를 반환한다.
이 JSX 요소들은 결국 컴포넌트 함수에 대한 함수 호출과 같다.

  1. DemoOutput 컴포넌트 함수 호출
  2. Button 컴포넌트 함수 호출

이것이 이유이다. 부모 컴포넌트가 변경이 되었으면 자식 컴포넌트 역시 함수 호출로인한 재실행이 된다. props의 값은 상관이 없다는것이다.

알수있는것은 함수에서 재평가가 된다고 실제 DOM이 변경된다는것이 아니라는것이다.
위에서 볼 수 있듯이, “앱 실행중”과 “데모 실행중”으로 함수가 재평가되는 모습을 끊임없이 볼 수 있는데도 불구하고 실제 DOM의 모습은 그대로다.


궁금증3 : 그렇다면 컴포넌트 함수가 자식,자손까지 재평가가 된다면 성능 저하가 일어나지 않는것인가??

위에서 보았던 코드를 또 다시 분리하여 App.js 기준에서 자손컴포넌트를 만들어주자
DemoOutput.js

import React from 'react';
import DemoOutputChild from './DemoOutputChild';
const DemoOutput = (props) => {
   console.log('데모 실행중');
   return <DemoOutputChild>{props.show ? 'This is new!' : ''}</DemoOutputChild>;
};
export default DemoOutput;

아래 코드는 자손컴포넌트를 담당한다.
DemoOutputChild.js

import React from 'react';
const DemoOutputChild = (props) => {
   console.log('Child 실행중');
   return <p>{props.children}</p>;
};
export default DemoOutputChild;

결과는

App(“앱실행중”)
DemoOutput(“데모 실행중”)
DemoOutputChild("Child 실행중")

부모, 자식, 자손 컴포넌트 함수 모두 재평가 되어 실행되었음을 알 수 있다.

이쯤되면 궁금해진다. 이렇게 부모 컴포넌트가 재 평가되어 실행된다면 자식컴포넌트들은 자동으로 재평가되어 실행이되는데 성능적인 부분은 문제가 없는것인가?
=> 그렇지는 않다. 실제 DOM에서는 바뀌는것이 없다. 이전 스냅샷과 최근 스냅샷에서 바뀌는점이 없으니 출력되는것도 같다는것이다.

그렇지만, 모든 자식 컴포넌트를 재 실행한다는것은 낭비의 문제이므로 다음 단원에서 이런 낭비를 줄이기위한 방법을 찾아보자.



13.3 : 불필요한 재평가 방지하기.

소규모 프로젝트에서는 가상비교까지 최적화하는것은 필수적인 작업이 아닐 수 있다.
하지만 대규모 프로젝트로 가서는 개발자들은 최적화를 신경쓰게되는데,
이번 단원에서 특정한 상황에서만 함수 컴포넌트를 실행하도록하는 방법을 알아보자.

13.3.1 : React.memo를 사용하여 불필요한 재평가 방지

React.memo를 사용해보자.
대상으로 할 컴포넌트를 지정한 뒤 래핑을 해주면 끝나는 단순한 방식이다.
자식 컴포넌트인 DemoOutput.js 함수 컴포넌트에 래핑해보자.
DemoOutput.js

import React from 'react';
import DemoOutputChild from './DemoOutputChild';
const DemoOutput = (props) => {
   console.log('데모 실행중');
   return <DemoOutputChild>{props.show ? 'This is new!' : ''}</DemoOutputChild>;
};

// 이 memo는 이 컴포넌트에 어떤 props가 입력되는지 확인한후
// 입력되는 모든 props의 신규 값을 확인하여 기존 값과 비교하도록 리액트에 전달
// 그리고 props의 값이 바뀐 경우에만 컴포넌트를 재실행 및 재평가 한다.
// 부모 컴포넌트가 변경되어도 props로 전달되어오는 값이 바뀌지 않는다면
// 컴포넌트 실행은 건너뛰게된다.
export default React.memo(DemoOutput);

최초 실행시 실행되는것을 제외하면 버튼을 계속 눌러줘도 “앱 실행중”만 출력되는것을 확인 할 수 있다.
DemoOutput이 실행되지 않으니 자식인 MyParagraph도 역시 실행되지 않음을 볼 수있다.



최적화를 위한 성능 비용도 존재한다.

그러면 모든 컴포넌트에 대해 Memo를 사용하면 되지 않나 궁금할텐데, 이는 적절하지는 않다. 최적화는 비용이 아예 0이 아니라 props에 대한 비교작업의 리소스도 필요하기 때문에 적절한 컴포넌트에 대해서 사용해야지만 비로소 최적화라고 할 수있을것이다.

Good Case : 자식 컴포넌트가 많을경우.
컴포넌트 트리상 상위에 위치해있고 자식 컴포넌트가 많다면, 이럴경우 쓰기 적절하다.

Bad Case : props의 값이 매번 변화될 여지가 있을경우.
값을 매번 변화될 여지가 있는데, 이에 대해서 굳이 비교작업을 할 필요가 없을 것이다. 그러므로 쓰기 부적절하다.



원시값과 객체값의 차이로인한 결과를 조심하자.

App.js에 있는 Button 컴포넌트에서..
App.js

<Button onClick={toggleParagrapHandler}>Toggle Paragraph</Button>
 

콘솔메시지를 띄우는 코드를 추가해주자.
Button.js

const Button = (props) => {
console.log(“버튼클릭");
return (
...
);
};

이런식으로 Toggle Paragraph를 클릭하면 “버튼클릭” 콘솔 출력창이 나온다.

이 Button도 Memo 래핑으로 감싸주면 “버튼클릭”이 안나오지 않을까?
props로 전달하는 onClick={toggleParagrapHandler} 는 항상 바뀌지 않는것처럼
보이기 때문이다. Memo 래핑한뒤 결과값을 보자.

여전히 똑같이 나온다.
이러한 이유는, 원시값과 객체의 참조값으로 인한 차이의 결과이다.
자바스크립트의 내용으로 아래의 링크를 참조하자.
원시 값과 객체의 비교



// props로 false라는 원시값을 가진다
<DemoOutput show={false}/>

해당 원시값을 비교 구문을 사용한다고 해보자.

console.log(false === false); // true

memo를 래핑한 컴포넌트를 리액트에서 가상비교를 할때
false(이전스냅샷) , false(최근스냅샷)을 비교한다.
false는 원시값으로 두개를 비교 할때, 이는 true의 결과값이 나오므로
해당 DemoOutput 함수 컴포넌트는 재실행이 되지 않는다.

하지만 객체값을 가진다면 문제가된다.

// props로 함수를 담고있는 객체참조값을 가진다. 
<Button onClick={toggleParagrapHandler}>Toggle Paragraph</Button>

해당 객체참조값을 비교 구문을 사용한다고 해보면 다음과 같이 비교를 한다고 할 수있다.

console.log( toggleParagrapHandler === toggleParagrapHandler);	// true

이를 바닐라 자바스크립트로 구동한다면 true가 나오게되지만,
리액트로 구동한다면 false가 나올것이다.
이전 스냅샷의 toggleParagrapHandler의 객체를 담고있는 메모리주소에대한 참조값은
최근 스냅샷의 toggleParagrapHandler의 객체를 담고있는 메모리주소에대한 참조값과 다르다는것이다.
이는 서로 다른 메모리주소에대한 참조값을 비교하므로 리액트에선 false의 값이 나오게된다는것이다.

경험 많은 개발자도 어려움을 겪고있는 이슈중 하나이므로 잘 알아두자.

13.3.2 : useCallback()으로 함수 재생성 방지.

최근 스냅샷의 함수가 이전 스냅샷의 함수의 참조값를 가리키게 할경우 문제 해결이 될것같다. 이럴 경우에 사용 하는것이 useCallback() 리액트 훅이다.

useCallback을 사용한다는것을 알리고..

import React, {useState , useCallback} from 'react';

useCallback 훅의 첫번째 인자로 함수를 집어넣는다.
const toggleParagrapHandler = useCallback( () => {
   setShowParagraph( prevShowParagraph => !prevShowParagraph);
  },[] );

두번째 인자는 의존성배열로 useEffect의 문법과 동일하다.
의존성 배열에 setShowParagraph을 넣을수는 있지만, useCallback 훅 자체가
해당 함수가 이전과 동일한 함수 객체임을 보장하므로 배열에 넣을 필요는 없다.
이렇게 빈 배열로 넣어주게되면 이 콜백함수는 변하지않고 항상 같은 함수가 사용되게끔하는 구문이된다.

13.3.3 : useCallback() 의 해당 종속성

  const toggleParagrapHandler = useCallback( () => {
   setShowParagraph( prevShowParagraph => !prevShowParagraph);
  },[] );

그렇다면 위 코드에서 useCallback에서 두번째 인자는 왜 필요한것일까?
내 함수는 실행할때마다 똑같은 로직을 사용한다. 즉 바뀔여지가 없는데 왜 필요할까?

자바스크립트의 클로저와 관련이 있다.

자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저(Closure)라고 부른다.

다음의 코드를 보자

App.js

import React, {useState , useCallback} from 'react';
import DemoOutput from './components/Demo/DemoOutput';
import Button from './components/UI/Button/Button';
import './App.css';

function App() {
  const [showParagraph, setShowParagraph] = useState(false);
  const [allowToggle, setAllowToggle] = useState(false);

  console.log('앱 실행중');

  const toggleParagrapHandler = useCallback( () => {
   if (allowToggle) {
    setShowParagraph( prevShowParagraph => !prevShowParagraph);  
   }
  },[] );

  //  다른 버튼에 대한 토글을 활성화함.
  const allowToggleHandler = () => {
   setAllowToggle(true);
  };

  return (
   <div className="app">
    <h1>Hi there!</h1>
    <DemoOutput show={showParagraph}/>
    <Button onClick={allowToggleHandler}>Allow Togging</Button>
    <div><br></br></div>
    <Button onClick={toggleParagrapHandler}>Toggle Paragraph</Button>
   </div>
  );

이 코드의 의도는
allowToggleHandler의 함수를 담고있는 버튼을 누르면 ( “setAllowToggle(true)“ )
그 이후로 toggleParagrapHandler의 함수를 담고 있는 버튼을 활성화 시키게끔 하려고 한다.

Allow Togging버튼을 클릭하여 Toggle Paragraph 버튼을 활성화시켜서 Toggle Paragraph
버튼을 눌러보았지만, 아무런 반응이없다.

Why? => 위에서 말했지만 , 클로저와 관련이 있다.

 const toggleParagrapHandler = useCallback( () => {
   if (allowToggle) {
    setShowParagraph( prevShowParagraph => !prevShowParagraph);  
   }
  },[] );

이 useCallback 함수가 정의될때! 해당함수의 외부에서 정의된 allowToggle의 변수를 기억한다. 이 useCallback 함수의 allowToggle은 false의 값을 계속 기억하는것이다.
const [allowToggle, setAllowToggle] = useState(false);

이것이 문제다. useCallback 함수를 사용하면 해당 함수는 메모리 어딘가에 저장이되는데, 이때 App 함수가 토글상태를 변경시키더라도 useCallback를 사용한 함수는 재생성되지 않는다. 우리가 어떤환경에서도 변경시키지 말라고 useCallback을 이용해 리액트에게 알렸기 떄문이다.

이를 해결하기위해서는 useCallback 함수의 두번째 인자에 의존성 배열을 조작해주면된다.

함수의 메모리 참조값을 지속적으로 유지하되(useCallback),
어떠한 상태가 변경이 될경우 함수 재생성을 해라 (useCallback의 두번째 인자 사용)

코드를 수정하여 의존성 배열에 allowToggle 를 넣어주자
App.js 

const toggleParagrapHandler = useCallback( () => {
   if (allowToggle) {
    setShowParagraph( prevShowParagraph => !prevShowParagraph);  
   }
  },[allowToggle] );

Allow Togging 버튼을 눌러서 Toggle Paragraph 버튼을 활성화시켜주고..

Toggle Paragraph 버튼을 눌렀더니

제대로 동작한다!!



13.4 : State및 컴포넌트

13.4.1 : State및 컴포넌트 자세히 살펴보기

해당 단원으로 다시한번 상기시키는것이지만, 리액트에서 상태란 가장 중요한 개념!

상태는 컴포넌트를 다시 렌더링하며, 화면에 표시되는 것들을 바꾼다.
컴포넌트와 상태의 상호작용은 리액트의 핵심개념이고, 리액트가 컴포넌트와 상태 둘다 관리한다.
상태 관리 중에서 가장 일반적인 형태는 useState Hook을 사용하는것이다.
이를 이용하면, 새로운 상태를 만들어서 자동적으로 useState를 호출한 컴포넌트에 연결이 가능한데, 이런 종류의 연결도 리액트가 담당하는 역할이다.

참고로 useState의 기본값, 즉 초기값은 한번만 고려되도록 처리한다. 다시말해서 App 컴포넌트가 최초 실행될때를 말한다.
useState가 실행되면 리액트가 관리하는 새로운 상태 변수를 만들게 된다. 그리고 난 뒤, 리액트는 이 변수가 어느 컴포넌트에 속하는지를 기억해둔다. 그리고 기본값을 사용해서 상태값을 초기화한다. 이후 App 함수 호출이 일어날 재평가하는 과정에서 useState가 호출되면 새로운 상태가 생성되지 않는다. 왜냐하면 리액트는 여기에 이미 이 컴포넌트에 대한 상태가 이미 존재하다는것을 깨닫기 때문이다. 필요한 경우에 이 상태가 업데이트는 될 수 있다.

따라서 리액트는 상태의 “관리” 와 “갱신”만을 담당한다고 볼 수 있다.

컴포넌트가 DOM 에서 완전히 삭제되거나 하지 않는 이상 상태의 초기화는 이루어지지 않는다. 반대로 말하면 컴포넌트가 삭제되었다가 다시 연결되었다면 새로운 상태가 초기화 될 수는 있다.
하지만 DOM에 컴포넌트가 연결되고 유지되는 동안에는 상태는 최초의 초기화 이후에 갱신만된다. (useReducer의 경우에도 마찬가지)

13.4.2 : State 스케줄링 및 일괄 처리 이해하기

위 사진을 해석하자면..
“MyProduct” 컴포넌트에 상태를 저장하려한다. 현재 상태는 "DVD"다.
여기서 사용자의 클릭으로인하여 SetNewProduct 함수트리거가 발생되는데, 상태를 “Book"으로 바뀌게된다.

사용자가 버튼을 클릭했다고 가정해보자.
상태는 즉각적으로 "DVD"로 바뀌지 않는다. 다만 "DVD"로 상태 업데이트를 하게끔 예약을 한다. 이것이 상태 갱신 예약이다.
의문이 들 수 있다. 우리 눈에는 즉각적으로 바뀌는것 같은데, 상태 업데이트는 예약되어 갱신이된다고 한다.
하지만 리액트 내부적으로는 우선순위를 두어서 프로세스를 처리한다. 예를들어 사용자 입력에 응답하는것은 당연하게도 화면의 문자를 변경하는 것보다는 우선순위가 높다.

그렇다면 스케쥴링 때문에 다수의 예약 상태 변화가 동시에 있을 수 있는데 이는 괜찮은것일까??
=> 그래서 함수 형태로 인해 갱신하는 형태를 사용한다. 특히 이전 스냅샷에 의존하는 경우에는 더더욱 사용해야한다.

함수 형태로 사용하는것이 다수의 예약 상태변화와 무슨 상관이길래??
=> 함수형태로 사용하지 않는다면 컴포넌트 함수의 실행중 마지막 무렵에서 상태를 얻을텐데, 이는 상태 변경이 순서대로 실행될때(함수 형태로 갱신하는 형태)와는 다른 결과가 나올 수 있기 떄문이다.
즉 함수형태로 관리하지않는 상태관리에서 완료되지 않은 상태 변경 작업이 여러개가 있다고 가정할때, App 컴포넌트의 재 렌더링 주기는 모두 동일하기 때문이다. 즉 상태 관리하는 순서를 내 마음대로 조작하는것이 되지 않는다.

State 스케쥴링을 완전 이해하지는 못했다. 추후 많은 코드들을 익혀가면서 반복적으로 학습해보자.

13.5 : useMemo()로 최적화하기

useMemo()는 “값”을 반환하는 함수다. 두번째 인자값이 변경된것을 감지하면 내부의 정의된 첫번째 인자값인 콜백함수를 실행하여 값을 리턴한다.
useMemo()는 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화 하는 용도로 사용이된다. 즉 불필요한 연산을 제어하는 용도로 사용되는 경우가 많다.
예를들면, 매번 함수컴포넌트가 재실행될때마다 정렬작업을 하는 “연산”은 불필요할 경우가 있을수 있다. 이때 사용하는 것이 useMemo()를 사용한다.

App.js

import React, { useState, useCallback, useMemo } from 'react';
import './App.css';
import DemoList from './components/Demo/DemoList';
import Button from './components/UI/Button/Button';
function App() {
  const [listTitle, setListTitle] = useState('My List');
  const changeTitleHandler = useCallback(() => {
   setListTitle('New Title');
  }, []);
// 데이터를 listItems로 저장한다.
  const listItems = useMemo(() => [5, 3, 1, 10, 9], []);
  return (
   <div className="app">
// DemoList 컴포넌트에게 데이터를 items의 이름으로 props전달
    <DemoList title={listTitle} items={listItems} />
    <Button onClick={changeTitleHandler}>Change List Title</Button>
   </div>
  );
}
export default App;

DemoList.js

import React, { useMemo } from 'react';
import classes from './DemoList.module.css';

const DemoList = (props) => {

// 데이터로 들어온것을 items 객체에 전달
  const { items } = props;

// 해당 데이터를 정렬 작업을 하면서도, 이후에 items가 바뀌지않으면
// 정렬작업을 실시하지도않고 결국 return의 값도 바뀌지 않는다.
  const sortedList = useMemo(() => {
   console.log('Items sorted');
   return items.sort((a, b) => a - b);
  }, [items]); 

  console.log('DemoList RUNNING');
  return (
   <div className={classes.list}>
    <h2>{props.title}</h2>
    <ul>
     {sortedList.map((item) => (
      <li key={item}>{item}</li>
     ))}
    </ul>
   </div>
  );
};
export default React.memo(DemoList);

실행전

Title이 "My List"인 상태에서 Change List Title 버튼을 누르게된다면..

실행후

“New Title” 로 바뀌게 되고, 이후 Change List Title을 눌러도 아무런 반응도 없고 이는 아무리 클릭을 하여도 정렬 작업을 더는 실시하지 않는다는것을 알 수 있다.

이와 비슷한 함수로 useCallback()는 “함수”를 반환하는 함수다. 두번째 인자값이 변경된것을 감지하면 내부의 정의된 첫번째 인자값인 콜백함수를 실행하여 함수를 리턴한다.

profile
Developer

0개의 댓글