상세 컨텐츠

본문 제목

[React.js 1] p1. 카운터 앱 만들기, 6장. 라이프 사이클과 리액트 개발자 도구

23-24/React.js 1

by YUZ 유즈 2023. 11. 17. 10:00

본문

728x90

카운터 앱 만들기

요구사항 분석하기

  • 현재의 카운트를 표시: 따라서 이 영역의 이름을 뷰어(Viewer) 라고 하겠습니다.
  • 카운트를 늘리거나 줄일 수 있는 6개의 버튼: 카운트를 제어하는 영역이라는 의미에서 컨트롤러(Controller)라고 이름 붙이겠습니다.

컴포넌트 단위로 생각하기

리액트에서 앱을 구현할 때는 컴포넌트 단위로 생각하는 게 필요합니다. 앞에서 살 펴본 Viewer, Controller 영역을 일종의 컴포넌트라고 생각하는 겁니다.

[카운 터] 앱에는 3개의 컴포넌트가 있습니다. 
  • App 컴포넌트: Viewer와 Controller 컴포넌트를 감싸는 템플릿 
  • Viewer 컴포넌트: 현재의 카운트를 표시함 
  • Controller 컴포넌트: 카운트를 제어할 수 있는 기능을 제공함

“하나의 컴포넌트는 단 하나의 역할만 수행한다.”

 

리액트 앱 만들기 

준비 과정의 마지막 단계는 리액트 앱을 만드는 작업입니다.
문서(Documents) 폴더 아래에 project1 폴더를 만든 다음, 비주얼 스튜디오 코드에서 엽니다. 계속해서 터미널을 열고 다음 명령어를 입력해 리액트 앱을 생성합 니다.
npx create-react-app .
src 폴더에서 사용하지 않는 다음 파일을 삭제합니다.
  • src/App.test.js
  • src/logo.svg
  • src/reportWebVitals.js
  • src/setupTest.js

사용하지 않는 코드 역시 삭제합니다.

 
모두 완료했다면 터미널에서 npm run start를 입력해 리액트 앱을 시작합니다.
 

 

UI 구현하기

UI는 사용자 인터페이스라는 뜻으로, 웹 페이지에서 사용자와 상호작용하는 요소를 말합니다. 이 요소들의 사용성을 높이기 위해 기능을 추가하기도 하고, 특별한 형태나 색상 등의 스타일을 적용하기도 합니다.

 

Viewer 컴포넌트 만들기

현재의 카운트를 표시하는 Viewer 컴포넌트를 만들겠습니다. 그 전에 src에 component 폴더를 만들고, 이 폴더에서 Viewer.js를 생성합니다.
계속해서 Viewer 컴포넌트를 다음과 같이 작성합니다.
const Viewer = () => {
  return (
    <div>
      <div>현재 카운트: </div>
      <h1>0</h1>
    </div>
  );
};
export default Viewer;
Viewer 컴포넌트를 페이지에 렌더링하기 위해서는 App의 자식으로 배치해야 합 니다. 이때 App 컴포넌트에서 페이지의 제목도 함께 추가하겠습니다.
App.js 파일을 다음과 같이 작성합니다.
import "./App.css";
import Viewer from "./component/Viewer"; ①

function App() {
  return (
    <div className="App">
      <h1>Simple Counter</h1> ②
      <section>
        <Viewer /> ③
      </section>
    </div>
  );
}

export default App;

 component 폴더에 있는 Viewer 컴포넌트를 불러옵니다.   제목 ‘Simple Counter’를 <h1> 태그로 감싸 페이지에서 렌더링합니다.   Viewer 컴포넌트를 불러와 <section> 태그로 감싸 렌더링합니다. <section>은 영역을 분리하기 위한 태그로 <div>와 동일한 기능을 수행합니다.

Viewer 컴포넌트 렌더링

 

Controller 컴포넌트 만들기

다음으로 카운트를 늘리거나 줄이는 Controller 컴포넌트를 만들겠습니다.
component 폴더에서 Controller.js를 만들고 다음과 같이 작성합니다.
const Controller = () => {
  return (
    <div>
      <button>-1</button>
      <button>-10</button>
      <button>-100</button>
      <button>+100</button>
      <button>+10</button>
      <button>+1</button>
    </div>
  );
};
export default Controller;

App 컴포넌트를 다음과 같이 수정합니다.

import "./App.css";
import Controller from "./component/Controller"; ①
import Viewer from "./component/Viewer";

function App() {
  return (
    <div className="App">
      <h1>Simple Counter</h1>
      <section>
        <Viewer />
      </section>
      <section>
        <Controller /> ②
      </section>
    </div>
  );
}

export default App;

컴포넌트 스타일링하기

자신이 원하는 스타일을 적용해도 좋습니다.

src 폴더 App.css에서 기존 스타일 규칙을 모두 삭제하고 원하는 대로 작성합니다.

body {
  padding: 20px;
}

.App {
  margin: 0 auto;
  width: 500px;
}

.App > section { ①
  padding: 20px;
  background-color: rgb(245, 245, 245);
  border: 1px solid rgb(240, 240, 240);
  border-radius: 5px;
  margin-bottom: 10px;
}
UI 구현은 모두 끝났습니다.
저장하고 지금까지 작업한 결과를 확인합니다.

[카운터] 앱 UI 최종 완료

 

기능 구현하기

 State는 반드시 컴포넌트 함수 안에 만들어야 합니다. 현재 여러분과 함께 만들고 있는 [카운터] 앱에는 App, Viewer, Controller 3개의 컴포넌트가 있습니다. 그렇다면 어떤 컴포넌트에서 [카운터] 앱의 State를 만들어야 할까요? 

 

⇒  App 컴포넌트

 

오답1: Viewer 컴포넌트=> Viewer 컴포넌트가 Controller 컴포넌트에 setCount를 전달할 방법이 없다.

오답2: Controller 컴포넌트=> 변경된 State 값을 Viewer 컴포넌트에 전달할 방법이 없기 때문입니다.

정답: App 컴포넌트

 

App.js를 다음과 같이 수정합니다.

import "./App.css";
import { useState } from "react";
import Controller from "./component/Controller";
import Viewer from "./component/Viewer";

function App() {
  const [count, setCount] = useState(0);
  const handleSetCount = (value) => {
    setCount(count + value);
  };

  return (
    <div className="App">
      <h1>Simple Counter</h1>
      <section>
        <Viewer count={count} /> ①
      </section>
      <section>
        <Controller handleSetCount={handleSetCount} /> ②
      </section>
    </div>
  );
}
export default App;

 Viewer 컴포넌트에 State 변수 count의 값을 Props로 전달합니다

  Controller 컴포넌트에 State 값을 변경하는 함수 setCount를 Props로 전달합니다

 

Viewer 컴포넌트에서 App에서 받은 Props를 페이지에 렌더링합니다.

const Viewer = ({ count }) => {
  return (
    <div>
      <div>현재 카운트 : </div>
      <h1>{count}</h1>
    </div>
  );
};
export default Viewer;

Controller.js를 다음과 같이 수정합니다.

const Controller = ({ handleSetCount }) => {
  return (
    <div>
      <button onClick={() => handleSetCount(-1)}>-1</button>
      <button onClick={() => handleSetCount(-10)}>-10</button>
      <button onClick={() => handleSetCount(-100)}>-100</button>
      <button onClick={() => handleSetCount(100)}>+100</button>
      <button onClick={() => handleSetCount(10)}>+10</button>
      <button onClick={() => handleSetCount(1)}>+1</button>
    </div>
  );
};
export default Controller;

 

지금까지 카운트 기능을 구현하려면 State를 App 컴포넌트에서 만들어야 한다는 점을 살펴보았습니다. 그 이유를 다시 정리하면 State 값은 Viewer 컴포넌트, set 함수는 Controller 컴포넌트에 전달해야 하기 때문입니다.
리액트는 State 값이나 set함수를 여러 컴포넌트에서 사용하는 경우, 이들을 상위 컴포넌트에서 관리합니다. 리액트에서는 이 기능을 다른 말로 ‘State 끌어올리기(State Lifting)’라고 합니다.

 

리액트답게 설계하기

리액트는 규모가 크고 빠른 웹 애플리케이션을 만들기 좋은 기술입니다. 이를 위해 리액트가 권장하는 애플리케이션 설계 방식에 대해 살펴보겠습니다. 
리액트에서 컴포넌트 간에 데이터를 전달할 때는 Props를 사용하는데, 전달 방향 은 언제나 부모로부터 자식에게 전달하는 방식입니다. 리액트의 이러한 데이터 전 달 특징을 ‘단방향 데이터 흐름’이라고 합니다. 
리액트의 단방향 데이터 전달은 데이터의 흐름을 이해하기 쉽고, 관리하기 좋다는 장점이 있습니다.
반면 State를 변경하는 이벤트는 자식에서 부모를 향해 역방향으로 전달되어야 합니다.
리액트에서 이벤트의 전달 방향

 


라이프 사이클과 리액트 개발자 도구

리액트 컴포넌트의 라이프 사이클은 크게 3단계로 구분합니다. 다음 그림은 리액트 컴포넌트의 3단계 라이프 사이클을 도식화한 것입니다. 

  • 마운트(Mount): 컴포넌트를 페이지에 처음 렌더링할 때 
  • 업데이트(Update): State나 Props의 값이 바뀌거나 부모 컴포넌트가 리렌더해 자신도 리렌더될 때 
  • 언마운트(Unmount): 더 이상 페이지에 컴포넌트를 렌더링하지 않을 때 

 

리액트 훅의 하나인 함수 useEffect를 이용하면 이 사이클을 쉽게 제어할 수 있습니다.

 

useEffect

첫 번째 프로젝트로 만든 카운터 앱을 수정하면서 함수 useEffect를 어떻게 사용하는지 알아보겠습니다.

 

하나의 값 검사하기

카운터 앱의 App 컴포넌트에서 State 변수 count의 값이 바뀌면, 변경된 값을 콘솔에 출력하겠습니다.
project1 폴더를 열어 App.js를 다음과 같이 수정합니다.
import { useState, useEffect } from "react"; ①
(...)

function App() {
  const [count, setCount] = useState(0);
  const handleSetCount = (value) => {
    setCount(count + value);
  };

  useEffect(() => { ②
    console.log("count 업데이트: ", count);
  }, [count]);

  return (
    <div className="App">
      <h1>Simple Counter</h1>
      <section>
        <Viewer count={count} />
      </section>
      <section>
        <Controller handleSetCount={handleSetCount} />
      </section>
    </div>
  );
}
export default App;
useEffect의 용법
useEffect(callback, [deps])
          콜백 함수 의존성 배열
두 번째 인수로 전달한 배열을 의존성 배열(Dependency Array, 줄여서 deps)이라고 하는데, useEffect는 이 배열 요소의 값이 변경되면 첫 번째 인수로 전달한 콜백함수를 실행합니다.
코드에서 useEffect의 의존성 배열 요소에 State 변수 count가 있으므로, 이 값이 바뀌면 콜백 함수가 실행됩니다. 콜백 함수는 콘솔에 ‘count 업데이트’라는 문자열과 함께 변경된 State 값을 출력합니다.
저장하고 개발자 도구의 콘솔을 확인합니다.

버튼을 클릭할 때마다 useEffect에 인수로 전달한 콜백 함수가 실행되어 변경된 State 값을 콘솔에 출력합니다. 이렇듯 useEffect를 이용하면 특정 값이 바뀔 때마다 여러분이 원하는 코드를 실행하도록 만들 수 있습니다.

 

여러 개의 값 검사하기

useEffect의 의존성 배열 요소가 여러 개 있어도 마찬가지입니다. 즉, 배열 요소 중 하나가 변경되어도 useEffect는 콜백 함수를 실행합니다. 
현재 카운터 앱의 App 컴포넌트에는 State 변수 count 외에는 변경할 수 있는 값이 없습니다. 따라서 임시로 입력 폼을 추가하고, 이 폼에 입력한 데이터를 처리하는 text라는 이름의 State 변수를 하나 더 만들겠습니다. 
App 컴포넌트를 다음과 같이 변경합니다.
(...)
function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState(""); ①
b
  const handleSetCount = (value) => {
    setCount(count + value);
  };
  const handleChangeText = (e) => { ②
    setText(e.target.value);
  };b

  useEffect(() => {
    console.log("count 업데이트: ", count);
  }, [count]);

  return (
    <div className="App">
      <h1>Simple Counter</h1>
      <section>
        <input value={text} onChange={handleChangeText} /> ③
      </section>b
      <section>
        <Viewer count={count} />
      </section>
      <section>
        <Controller handleSetCount={handleSetCount} />
      </section>
    </div>
  );
}
export default App;
  return (
    <div>
      <input ref={textRef} value={text} onChange={handleOnChange} />
      <button onClick={handleOnClick}>작성 완료</button>
    </div>
  );
}
export default Body;

 useState를 이용해 State 변수 text를 만듭니다.   onChange 이벤트 핸들러 함수 handleTextChange를 만듭니다.  새로운 <section> 태그로 페이지의 영역을 나누고, 텍스트 입력 폼을 생성합니다. 사용자 입력을 처리하기 위한 value 속성에 변수 text를 전달하고, onChange 이벤트 핸들러로 함수 handleChangeText를 지정합니다.

 

 

이제 text 값이 변경되어도 useEffect가 콜백 함수를 실행해야 합니다. App 컴포넌트를 다음과 같이 수정합니다

(...)
function App() {
  (..)
  useEffect(() => {
    console.log("업데이트: ", text, count);
  }, [count, text]);
  (..)
}
export default App;

 useEffect의 콜백 함수가 실행되면 State 변수 text와 count 값을 콘솔에 출력합니다.   useEffect에 전달하는 의존성 배열에 변수 text를 요소로 추가합니다.

 

두 개의 State 값이 변할 때마다 콘솔에 값을 출력합니다.

 

 

컴포넌트의 마운트 제어하기 

이번에는 컴포넌트의 마운트 시점에 실행되는 코드를 작성하겠습니다. 이를 “컴포넌트의 마운트를 제어한다”라고 표현합니다. App 컴포넌트를 다음과 같이 수정합니다.
(...)
function App() {
  (...)
  const didMountRef = useRef(false); 

  useEffect(() => { 
    if (!didMountRef.current) {
      didMountRef.current = true;
      return;
    } else {
      console.log("컴포넌트 업데이트!");
    }
  });

  useEffect(() => { ①
    console.log("컴포넌트 마운트");
  }, []);

	return (
	  (...)
	);
}
export default App;

 

컴포넌트 언마운트 제어하기 

라이프 사이클의 마지막 단계인 언마운트는 컴포넌트가 페이지에서 제거될 때입니다. 컴포넌트 언마운트 시점에 필요한 코드 실행 방법을 살펴보겠습니다.

클린업

리액트 컴포넌트의 언마운트 시점을 제어하기 위해서는 먼저 클린업(Cleanup) 기능을 이해해야 합니다. 클린업이란 원래 ‘청소’라는 뜻입니다. 프로그래밍에서 이 개념은 특정 함수가 실행되고 종료된 후에, 미처 정리하지 못한 사항을 처리하는 일입니다. 
먼저 App 컴포넌트에서 다음과 같이 함수 useEffect를 한 번 더 호출합니다.
(...)
function App() {
  (...)
  useEffect(() => { 
    const intervalID = setInterval(() => { ①
      console.log("깜빡");
    }, 1000);
    return () => { ②
      console.log("클린업");
      clearInterval(intervalID); ③
    };
  });
	(...)
}
export default App;

 함수 setInterval은 새 인터벌을 생성하면 인터벌 식별자(id)를 반환합니다. 이 id를 변수 intervalID에 저장합니다.   useEffect에 인수로 전달한 콜백 함수가 새 함수를 반환하도록 합니다. 이 함수는 클린업 함수로 서 useEffect의 콜백 함수가 실행되기 전이나 컴포넌트가 언마운트하는 시점에 실행됩니다.   클린업 함수는 clearInterval을 호출합니다. 인수로 에서 생성한 인터벌 식별자를 전달해 앞서 생성한 인터벌을 삭제합니다.

 

정리하면 useEffect의 콜백 함수가 반환하는 함수를 클린업 함수라고 합니다. 이 함수는 콜백 함수를 다시 호출하기 전에 실행됩니다. 따라서 컴포넌트를 렌더링할 때 마다 새 인터벌을 생성하고 기존 인터벌은 삭제합니다.

 

 

리액트 개발자 도구

리액트 앱을 개발할 때 매우 유용하게 사용하는 리액트 개발자 도구(React Developer Tools)를 소개하려 합니다.

리액트 개발자 도구는 확장 도구로서 크롬 브라우저에 설치해 사용합니다.
 

[Chrome 웹 스토어] 페이지 ‘확장 프로그램 검색’ 폼에서 ‘React Developer Tools’라고 입력하고 <Enter>키를 누릅니다. [chrome 웹 스토어] 확장 프로그램 페이지가 나옵니다.

 

검색 결과로 나온 프로그램에서 첫 번째에 있는 React Developer Tools를 클릭합니다.
React Developer Tools 상세 페이지로 이동합니다. 상세 페이지 우측에 보이는 파란색 <Chrome에 추가> 버튼을 클릭합니다.
 

모든 설치를 완료했으면 확장 프로그램의 설정을 확인해야 합니다. 크롬 브라우저 우측 상단의 확장 아이콘을 선택하면 나오는 메뉴에서 [도구 더보기]-[확장 프로그램]을 차례로 클릭합니다.

방금 설치한 React Developer Tools를 찾아 스위치를 On으로 설정한 다음, <세부정보> 버튼을 클릭합니다.

[세부 정보] 페이지가 나옵니다. 세부 정보 페이지에서 다음과 같이 옵션을 설정합니다.
  • 사용: ON
  • 사이트 엑세스: 모든 사이트에서
  • 시크릿 모드에서 허용: 시크릿 모드에서 리액트 개발자 도구를 사용하려면 ON, 그렇지 않으면 자유롭게 설정합니다. 잘 모르겠다면 ON으로 설정합니다.
  • 파일 URL에 대한 엑세스 허용: ON

마지막으로 브라우저 우측 상단에서 퍼즐 모양으로 생긴 ‘확장 프로그램’ 아이콘을 클릭해 React Developer Tools의 핀을 고정합니다.

 

리액트 개발자 도구의 기능 사용하기 

설치를 모두 마쳤다면 리액트 개발자 도구가 제공하는 여러 유용한 기능을 실제로 사용해 보겠습니다. 

컴포넌트 트리 살펴보기 

컴포넌트 트리는 리액트 개발자 도구에서 가장 많이 사용하는 기능입니다. 개발자 도구의 [Components] 탭을 클릭합니다. 
[Components] 탭은 현재 렌더링된 카운터 앱의 컴포넌트 트리를 시각적으로 보여주고 있습니다. 컴포넌트 트리를 보면 App를 루트 컴포넌트로 하여 그 아래에 Viewer, Even, Controller 3개의 자식 컴포넌트가 있는 것을 볼 수 있습니다.
App를 클릭하면 해당 컴포넌트의 Props와 State를 실시간으로 모니터링할 수 있습니다. 리액트 훅의 기능인 State, Ref, Effect 등이 표시된 것도 확인할 수 있습니다.
 

리렌더 하이라이트 기능 사용하기

이번에는 리렌더가 발생한 컴포넌트를 하이라이트하는 기능을 사용하겠습니다.
이 기능을 이용하면 어떤 컴포넌트가 의미 없이 리렌더되는지 알 수 있어 향후 렌더링 최적화에 크게 도움을 줍니다. [Components] 탭 검색 폼 옆에 있는 톱니 모양의 View Settings 아이콘을 클릭합니다.
 
View Setting 아이콘을 클릭하면 별도 설정이 가능한 대화상자가 나타납니다.
대화상자 [General] 탭에서 “Highlight updates when component render.”라고 쓰여 있는 체크박스에 표시합니다.
이 기능을 활성화하면 컴포넌트를 렌더링할 때마다 하이라이트가 나타납니다.
 

Quiz

  1. ( UI )는 사용자 인터페이스라는 뜻으로, 웹 페이지에서 사용자와 상호작용하는 요소를 말합니다.
  2. 리액트 컴포넌트의 언마운트 시점을 제어하기 위해 사용하는, 특정 함수가 실행되고 종료된 후에, 미처 정리하지 못한 사항을 처리하는 일을 (클린업 ) 이라고 부릅니다.
  3. 리액트는 State 값이나 set함수를 여러 컴포넌트에서 사용하는 경우, 이들을 (상위 컴포넌트 )에서 관리합니다. 리액트에서는 이 기능을 다른 말로 ‘State 끌어올리기(State Lifting)’라고 합니다.
  4. 리액트 컴포넌트의 라이프 사이클 중 컴포넌트를 페이지에 처음 렌더링할 때 : ( 마운트(Mount) 
  5. 리액트 컴포넌트의 라이프 사이클 중 State나 Props의 값이 바뀌거나 부모 컴포넌트가 리렌더해 자신도 리렌더될 때   : ( 업데이트(Update) )
  6. 리액트 컴포넌트의 라이프 사이클 중 더 이상 페이지에 컴포넌트를 렌더링하지 않을 때   : ( 언마운트(Unmount) )
  7. 리액트 훅의 하나인 함수 ( useEffect )를 이용하면 리액트의 라이프 사이클을 쉽게 제어할 수 있습니다.

코드작성

1.

다음 요구사항에 따라 Counter 컴포넌트를 작성하세요.

  • 초기값이 0인 카운터를 표시합니다.
  • "Increment" 버튼을 누를 때마다 카운터가 1씩 증가합니다.
  • "Decrement" 버튼을 누를 때마다 카운터가 1씩 감소합니다.

 

답안:

더보기
import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };

  handleDecrement = () => {
    this.setState({ count: this.state.count - 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleIncrement}>Increment</button>
        <button onClick={this.handleDecrement}>Decrement</button>
      </div>
    );
  }
}

export default Counter;

 

2. 위 코드에 라이프 사이클 메서드를 추가하세요.

 

답안:

더보기
import React, { useState, useEffect } from 'react';

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

  const handleIncrement = () => {
    setCount(count + 1);
  };

  const handleDecrement = () => {
    setCount(count - 1);
  };

  // componentDidMount와 componentWillUnmount 역할
  useEffect(() => {
    console.log('Component Did Mount');

    // componentWillUnmount 역할
    return () => {
      console.log('Component Will Unmount');
    };
  }, []); // 빈 배열을 전달하여 componentDidMount에서만 실행되도록 설정

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

export default Counter;

출처 :  이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023)

Corner React.js 1

Editor: dalpaeng4

728x90

관련글 더보기