[React 🔯] 06. 프로젝트 - 카운터 앱
![[React 🔯] 06. 프로젝트 - 카운터 앱](/images/useBlog/react.jpg)
간단한 Counter App 만들어보기
지금까지 배운 컴포넌트, Props, State 개념을 활용해서 간단한 카운터 앱을 만들어보자. 버튼을 클릭하면 숫자가 증가하거나 감소하는 앱으로, 컴포넌트 분리와 State 관리를 실습하기에 좋은 예제다.
UI 구현
먼저 기능 없이 화면 구조만 잡아보자. 카운트 숫자를 보여주는 Viewer와 버튼 묶음인 Controller, 두 개의 컴포넌트로 분리한다.
components/Viewer.jsx
const Viewer = () => {
return (
<div>
<div>현재 카운트 :</div>
<h1>0</h1>
</div>
);
};
export default Viewer;
현재 카운트 값을 화면에 보여주는 컴포넌트다. 기능 구현 단계에서 props로 실제 count 값을 받아오도록 수정한다.
components/Controller.jsx
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;
카운트를 조작할 버튼들을 모아놓은 컴포넌트다. 클릭 이벤트는 아직 연결하지 않은 상태이고, UI 구조를 먼저 잡고 기능은 이후에 붙이는 방식으로 개발한다.
src/App.jsx
import './App.css';
import Viewer from './components/Viewer';
import Controller from './components/Controller';
function App() {
return (
<div className="App">
<h1>Simple Counter</h1>
<section>
<Viewer />
</section>
<section>
<Controller />
</section>
</div>
);
}
export default App;
루트 컴포넌트인 App에서 Viewer와 Controller를 불러와 배치한다. 각각을 <section> 태그로 감싸 영역을 구분했다. 이 시점에서는 두 컴포넌트가 서로 연결되지 않은 독립적인 UI 덩어리다.
src/App.css
body {
padding: 20px;
}
.App {
margin: 0 auto;
width: 400px;
}
.App > section {
background-color: rgb(245, 245, 245);
border: 1px solid rgb(240, 240, 240);
border-radius: 5px;
padding: 20px;
margin-bottom: 10px;
}
앱 전체 너비를 400px로 고정하고 margin: 0 auto로 가운데 정렬한다. section 태그에는 연한 배경색과 테두리, 둥근 모서리를 적용해 카드처럼 보이도록 스타일링했다.
기능 구현하기
계층구조
UI를 완성했으니 이제 기능을 붙여보자. 그 전에 컴포넌트 간의 관계를 먼저 파악해야 한다.

현재 구조는 App이 부모이고, Viewer와 Controller가 형제 컴포넌트다.

Props는 부모에서 자식으로만 전달할 수 있기 때문에, 형제 컴포넌트끼리 직접 데이터를 주고받는 것은 불가능하다. Controller에서 버튼을 클릭했을 때 Viewer의 숫자가 바뀌어야 하는데, 이 둘은 형제 관계이므로 직접 연결할 수 없다. 이 문제를 해결하는 방법이 바로 **State Lifting(State 끌어올리기)**이다.

count State를 두 컴포넌트의 공통 부모인 App으로 올려서 관리하고, 각각 필요한 값과 함수를 props로 내려주는 방식이다.

- count State → App에서 관리 → Viewer에 props로 전달
- onClickButton 함수 → App에서 정의 → Controller에 props로 전달
이제 코드에 적용해보자.
App.jsx
import './App.css';
import { useState } from 'react';
import Viewer from './components/Viewer';
import Controller from './components/Controller';
function App() {
const [count, setCount] = useState(0);
const onClickButton = (value) => {
setCount(count + value);
};
return (
<div className="App">
<h1>Simple Counter</h1>
<section>
<Viewer count={count} />
</section>
<section>
<Controller onClickButton={onClickButton} />
</section>
</div>
);
}
export default App;
count State를 App에서 관리한다. onClickButton 함수는 버튼에서 전달받은 value를 현재 count에 더해 State를 업데이트한다. value가 음수면 감소, 양수면 증가한다.
<Viewer count={count} />로 현재 카운트 값을 Viewer에 내려주고, <Controller onClickButton={onClickButton} />로 버튼 클릭 핸들러를 Controller에 내려준다.
이렇게 하면 Controller에서 버튼을 클릭할 때 App의 State가 바뀌고, 바뀐 State가 Viewer에 전달되어 화면이 업데이트된다.
Viewer.jsx
const Viewer = ({ count }) => {
return (
<div>
<div>현재 카운트 :</div>
<h1>{count}</h1>
</div>
);
};
export default Viewer;
App으로부터 count prop을 받아 {count}로 화면에 렌더링한다. 하드코딩된 0 대신 실제 State 값이 표시되므로, App에서 count가 변경될 때마다 Viewer도 자동으로 리렌더링되어 최신 값을 보여준다.
Controller.jsx
const Controller = ({ onClickButton }) => {
return (
<div>
<button
onClick={() => {
onClickButton(-1);
}}
>
-1
</button>
<button
onClick={() => {
onClickButton(-10);
}}
>
-10
</button>
<button
onClick={() => {
onClickButton(-100);
}}
>
-100
</button>
<button
onClick={() => {
onClickButton(+100);
}}
>
+100
</button>
<button
onClick={() => {
onClickButton(+10);
}}
>
+10
</button>
<button
onClick={() => {
onClickButton(+1);
}}
>
+1
</button>
</div>
);
};
export default Controller;
App으로부터 onClickButton 함수를 prop으로 받아 각 버튼의 onClick에 연결한다. 버튼마다 클릭 시 해당 숫자를 인수로 넘겨서 호출한다. 예를 들어 -1 버튼을 누르면 onClickButton(-1)이 실행되고, App의 setCount(count + (-1))이 호출되어 카운트가 1 감소한다.
화살표 함수 () => { onClickButton(-1) } 형태로 감싸는 이유는, 인수를 넘기면서 함수를 즉시 실행하지 않고 클릭 시점에 실행되도록 하기 위해서다.