React.js/한 입 크기로 잘라먹는 React.js

[P1-3. 카운터 앱 만들기] 기능 구현하기

해갈 2023. 6. 2. 17:46

UI 구현을 모두 마쳤으므로 이 UI 요소들을 움직이게 하는 카운터 기능들을 차레로 구현하겠습니다.

 

State 를 이용해 카운터 기능 구현하기

카운터의 기능을 한 문장으로 정의하면 다음과 같습니다.

 

"Controller 컴포넌트에 있는 버튼을 클릭하면, Viewer 컴포넌트에 있는 카운트가 증가하거나 감소해야 한다."

 

예를 들어 Controller 컴포넌트에 있는 <+100> 버튼을 클릭하면 Viewer 컴포넌트의 숫자는 0 에서 100 으로 바뀌어야 합니다.

 

버튼을 클릭하면 카운트 값이 변합니다.

 

버튼 클릭 이벤트가 발생했을 때 컴포넌트 값을 도적으로 렌더링하려면 리액트의 State 를 사용해야 합니다. 그렇다면 [카운트] 앱에서 State 를 사용해 어떻게 컴포넌트의 값을 동적으로 렌더링하는지 그 과정을 간단히 설명해 보겠습니다.

 

바로 실습을 진행할 수도 있지만 이 과정을 머릿속에서 그려보는 게 훨씬 도움을 줍니다.

 

1. 먼저 카운트를 관리할 State 를 만들고 초깃값을 0 으로 설정합니다.

2. 다음으로 Controller 컴포넌트의 버튼을 클릭하면 현재 State 값을 버튼이 전달하는 값과 계산해 변경합니다.

3. 다음으로 변경된 State 값은 Viewer 컴포넌트에 전달되어 페이지의 카운트 값을 업데이트합니다.

 

State 를 이용해 카운트 기능 구현하기

 

다음 과정에서 중요한 점을 하나 짚어보고 가겠습니다.

 

앱을 설계하는 데 꼭 필요한 사고실험 같은 겁니다.

 


State 는 어떤 컴포넌트에 만들까?

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

 

정답은 App 컴포넌트입니다.

 

왜 그럴까요? 정답인 이유를 확실히 아는 좋은 방법은 오답을 선택해 보고 무엇이 문제인지 직접 느껴보는 겁니다.

 

Viewer 또는 Controller 컴포넌트에 State 를 만들고 이 State 를 이용해 카운트 기능을 구현하면 어떤 문제가 생기는지 살펴보겠습니다.

 

 

오답1: Viewer 컴포넌트

 

Viewer 컴포넌트에서 [카운트] 앱에 사용할 State 를 만듭니다. Viewer.js 를 다음과 같이 수정합니다.

 

src/component/Viewer.js

import { useState } from "react";

const Viewer = () => {
  const [count, setCount] = useState(0); ①
  return (
    <div>
      <div>현재 카운트: </div>
      <h1>{count}</h1> ②
    </div>
  );
};

export default Viewer;
① useState 를 이용해 State 변수 count 를 만듭니다.
② count 값을 페이지에 렌더링합니다.

 

Viewer 컴포넌트에서 State 를 만들고 값을 렌더링했습니다.

 

이제 Controller 컴포넌트에서 버튼을 클릭하면 set 함수인 setCount 를 호출해야 합니다. 그런데 여기서 문제가 있습니다.

 

Viewer 컴포넌트가 Controller 컴포넌트에 setCount 를 전달할 방법이 없다는 겁니다. 5장에서 살펴보았듯이 리액트에서 컴포넌트가 다른 컴포넌트에 데이터를 전달할 때는 Props 를 사용하는데, Props 는 부모만이 자식에게 전달할 수 있습니다. Viewer 와 Controller 컴포넌트는 부모-자식 관계가 아니므로 어떠한 값도 전달할 수 없습니다.

 

Viewer 컴포넌트에서 State 를 만들 수 없는 이유

 


오답 2: Controller 컴포넌트

 

이번에는 Controller 컴포넌트에 [카운터] 앱에 사용할 State 를 만듭니다. Controller.js 를 다음과 같이 수정합니다.

 

src/component/Controller.js

import { useState } from "react";
const Controller = () => {
  const [count, setCount] = useState(0);
  const handleSetCount = (value) => { ①
    setCount(count + value);
  };

  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;
① 버튼에서 클릭 이벤트가 발생하면 호출되는 이벤트 핸들러 handleSetCount 를 만듭니다. 이 함수에서는 set ㅎ마수 setCount 를 호출하는데, 인수로 현재 State(count) 값과 매개변수 value 값을 더해 전달합니다.
② ~ ⑦ 6개의 버튼은 모두 클릭 이벤트가 발생하면 이벤트 핸들러 handleSetCount 를 호출합니다. 해당 버튼의 숫자를 인수로 전달합니다.  

 

버튼을 클릭하면 State 는 기존 값에서 해당 버튼의 숫자와 계산한 값으로 변경됩니다. 그러나 여기서도 문제가 있습니다. 변경된 State 값을 Viewer 컴포넌트에 전달할 방법이 없기 때문입니다.

 

다시 말해 State 변수 count 를 Viewer 컴포넌트에 전달해야 하는데, Viewer 와 Controller 컴포넌트는 자식-부모 관계가 아니므로 그렇게 할 수 없습니다.

 

Controller 컴포넌트에서 State 를 만들 수 없는 이유

 


정답: App 컴포넌트

 

Viewer, Controller 모두 [카운터] 앱의 State 가 있을 컴포넌트가 아니라는 것을 확인했습니다. 이번에는 정답인 App 컴포넌트에서 State 를 만들고 카운트 기능을 완성했습니다. 

 

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

 

src/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 를 페이지에 렌더링합니다.

 

src/component/Viewer.js

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

 

App 컴포넌트에서 받은 Props 를 페이지에 렌더링합니다. 5장에서 살펴보았듯이 리액트에서는 부모가 리렌더되거나 전달된 Props 가 변경되면 자식 컴포넌트도 자동으로 리렌더됩니다.

 

따라서 Viewer 컴포넌트는 Props 로 받은 State 값이 변경될 때마다 리렌더되어 실시간으로 이 값을 페이지에 렌더링합니다.

 

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

 

src/component/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;

 

App 컴포넌트에서 함수 handleSetCount 를 받아 버튼의 이벤트 핸들러로 사용합니다. 버튼을 클릭하면 함수 handleSetCount 를 호출하는데, 이 함수는 App 컴포넌트의 State 값을 업데이트합니다.

 

저장하고 카운트 기능이 잘 구현되는지 확인합니다.

 

[카운터] 앱의 최종 구현

 

지금까지 카운트 기능을 구현하려면 State 를 App 컴포넌트에서 만들어야 한다는 점을 살펴보았습니다. 그 이유를 다시 정리하면 State 값은 Viewer 컴포넌트, set 함수는 Controller 컴포넌트에 전달해야 하기 때문입니다.

 

리액트는 State 값이나 set 함수를 여러 컴포넌트에서 사용하는 경우, 이들을 상위 컴포넌트에서 관리합니다. 리액트에서는 이 기능을 다른 말로 'State 끌어올리기(State Lifting)' 라고 합니다.

 


리액트답게 설계하기

리액트는 규모가 크고 빠른 웹 애플리케이션을 만들기 좋은 기술입니다. 이를 위해 리액트가 권장하는 애플리케이션 설계 방식에 대해 살펴보겠습니다.

 

리액트에서 컴포넌트 간에 데이터를 전달할 때는 Props 를 사용하는데, 전달 방향은 언제나 부모로부터 자식에게 전달하는 방식입니다. 리액트의 이러한 데이터 전달 특징은 '단방향 데이터 흐름' 이라고 합니다.

 

리액트에서 데이터의 전달 방향

 

데이터를 항상 아래로 전달하는 단방향 데이터 흐름은 모든 자동차가 같은 방향으로만 달리는 일방통행 차선을 연상하게 합니다. 모든 자동차가 한 방향으로만 달린다면, 초보 운전자 입장에서는 운전하기가 수월하며, 교통상황도 한 눈에 확인할 수 있어 편합니다.

 

리액트의 단방향 데이터 전달은 흐름을 이해하기 쉽고, 관리하기 좋다는 장점이 있습니다.

 

반면 State 를 변경하는 이벤트는 자식에서 부모를 향해 역방향으로 전달되어야 합니다.

 

리액트에서 이벤트의 전달 방향

 

이번에 만들어본 간단한 [카운터] 앱에서는 Controller 컴포넌트에 있는 버튼 요소를 클릭할 때마다 App 컴포넌트의 State 를 업데이트하는 이벤트가 발생합니다.

 

App 컴포넌트는 자신이 관리하는 State 를 변경하는 함수를 Props 로 전달해 자식이 부모의 State 를 대신 업데이트하게 했습니다.

 

결론적으로 리액트 앱을 설계할 때는 데이터는 위에서 아래로, 이벤트는 아래에서 위로 향하도록 설계해야 합니다.