지금까지 리액트에 대해 학습한 내용을 바탕으로 카운터 앱을 만들어 보자.
카운터 앱은 간단한 덧셈과 뺄셈 기능만 갖추고 있으므로, 첫 리액트 프로젝트에 적합하다.
프로젝트 구현에 앞서 먼저 어떤 설계와 기능을 구현할지 살펴보는 일이 필요하다. 이를 소프트웨어 공학에서는 요구사항 분석이라고 한다. 이 파트에서는 요구사항을 분석하면서 카운터 앱 프로젝트를 준비한다.
[ 요구사항 분석 ]
이 앱은 1개의 페이지에서 제목, Viewer (현재 카운트 표시 영역), Controller(카운트 제어 영역) 세 부분으로 나뉜다.
[ 컴포넌트 단위로 생각하기 ]
앞서 언급한 부분 중 Viewer와 Controller 영역을 컴포넌트로 취급하면 이 앱은 다음의 세 가지 컴포넌트로 구성된다.
컴포넌트는 블록의 각 조각과 같이 단 하나의 기능만 수행하는 모듈로, 1. 재사용 가능하고 2. 독립된 모듈이어야 한다.
[ 리액트 앱 만들기 ]
5장에서 리액트 앱을 생성하였던 방식과 똑같이 생성할 수 있다.
Documents) 폴더 아래에 project1 폴더를 만든 다음, VS Code에서 터미널을 열고 다음 명령어를 입력하면 리액트 앱이 생성된다.
npx create-react-app .
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
import "./App.css";
function App() {
return <div className="App"></div>;
}
export default App;
마지막으로 터미널에서 명령어 npm run start를 입력하면 드디어 리액트 앱을 시작할 수 있다.
리액트 앱 생성을 마친 후, 이번에는 UI(User Interface) 구현을 진행한다. UI는 사용자와 상호작용하는 요소를 뜻하며, 기능 구현에 앞서 페이지의 외양(껍데기)을 구성하는 것이다.
[ Viewer 컴포넌트 만들기 ]
먼저 현재 카운트 값을 표시하는 역할인 Viewer를 생성한다. src 폴더 내에 component 폴더를 생성하고, component 폴더 내에 Viewer.js 파일을 생성하여 다음과 같이 작성한다.
const Viewer = () => {
return (
<div>
<div>현재 카운트: </div>
<h1>0</h1>
</div>
);
};
export default Viewer;
Viewer 컴포넌트를 페이지에 렌더링하고 제목을 포함시키기 위해 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;
페이지에 렌더링이 된 것을 확인할 수 있다.
[ 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;
Controller 컴포넌트를 페이지에 렌더링하고 제목을 포함시키기 위해 App.js 파일을 다음과 같이 수정한다.
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;
6개의 버튼이 한 줄로 렌더링된다.
[ 컴포넌트 스타일링 ]
마지막으로 요구사항에 맞는 스타일 규칙을 정의한다. 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;
}
.App > section은 App 컴포넌트의 최상위 <section> 태그에만 스타일을 적용하는 CSS 문법이다. 저장 후 작업 결과를 확인한다.
Viewer와 Controller 컴포넌트를 렌더링하고 적절한 스타일링을 적용하여 UI가 잘 구현된 것을 확인할 수 있다.
[ State를 이용해 카운터 기능 구현 ]
카운터 앱의 핵심 기능은 "Controller 컴포넌트의 버튼을 클릭하면, Viewer 컴포넌트에 있는 숫자가 증가하거나 감소한다."는 것이다.
이처럼 버튼 클릭 이벤트가 발생했을 때 컴포넌트 값을 동적으로 렌더링하려면 리액트의 State를 사용해야 한다. 그렇다면 State는 어디에 만들어야 할까?
[ State는 어떤 컴포넌트에 만들까? ]
앞서 소개했듯이 앱의 구조는 App, Viewer, Controller 3개의 컴포넌트로 이루어져 있다.
1. Viewer에 State를 만드는 경우 :
Props로 set 함수를 전달할 수 없으므로 Controller 컴포넌트에 setCount를 전달할 수 없다.
2. Controller에 State를 만드는 경우 :
변경된 State 변수 값을 Viewer 컴포넌트에 전달할 수 없다.
따라서 State는 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를 렌더링한다. Viewer.js를 다음과 같이 수정한다.
const Viewer = ({ count }) => {
return (
<div>
<div>현재 카운트 : </div>
<h1>{count}</h1>
</div>
);
};
export default Viewer;
다음으로 Controller는 Props로 받은 handleSetCount 함수를 호출하여 버튼 클릭 이벤트를 처리하도록 한다. 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 값은 Viewer 컴포넌트, set 함수는 Controller 컴포넌트에 전달해야 하므로 State는 App 컴포넌트에서 만들어야만 한다.
리액트에서는 여러 컴포넌트가 동일한 State 값이나 set함수를 사용해야 한다면, 이들을 공통 상위 컴포넌트로 끌어올려 관리한다. 이를 State 끌어올리기(State Lifting)라고 한다.
[ 리액트답게 설계하기 ]
리액트의 애플리케이션 설계 방식은 다음과 같다.
컴포넌트 간에 데이터를 전달할 때는 Props를 사용하며, 전달 방향은 언제나 부모로부터 자식에게 전달하는 방식이다. 이를 ‘단방향 데이터 흐름’이라고 한다. 반면 State를 변경하는 이벤트는 자식에서 부모를 향해 역방향으로 전달되어야 한다.
이 장에서는 리액트답게 데이터 전달 방식을 설계하고, 컴포넌트 간 데이터 흐름과 이벤트 처리를 알아보았다.
다음 이미지는 리액트 컴포넌트의 3단계 라이프 사이클을 나타낸다.
함수 useEffect는 어떤 값이 변경될 때마다 특정 코드를 실행하여 “특정 값을 검사”하는 리액트 훅이다.
[ 하나의 값 검사하기 ]
App 컴포넌트에서 State 변수 count의 값이 바뀌면, 변경된 값을 콘솔에 출력하도록 한다. 이를 위해 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를 호출하고 두 개의 인수를 전달한다. 첫 번째 인수로 콜백 함수, 두 번째 인수로 배열을 전달한다.
useEffect(callback, [deps])
콜백 함수 의존성 배열
두 번째 인수로 전달한 배열을 의존성 배열(Dependency Array, deps)이라고 하는데, useEffect는 이 배열 요소의 값이 변경되면 첫 번째 인수로 전달한 콜백 함수를 실행한다.
개발자 도구의 콘솔을 이용하여 콜백 함수가 값을 변경하는 것을 확인할 수 있다.
[ 여러 개의 값 검사하기 ]
배열 요소 중 하나가 변경되어도 useEffect는 콜백 함수를 실행한다. App.js를 다음과 같이 수정한다.
(...)
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleSetCount = (value) => {
setCount(count + value);
};
const handleChangeText = (e) => {
setText(e.target.value);
};
useEffect(() => {
console.log("업데이트: ", text, count);
}, [count, text]);
return (
<div className="App">
<h1>Simple Counter</h1>
<section>
<input value={text} onChange={handleChangeText} />
</section>
<section>
<Viewer count={count} />
</section>
<section>
<Controller handleSetCount={handleSetCount} />
</section>
</div>
);
}
export default App;
이를 통해 새로 생성한 text 변수의 값이 변경되어도 useEffect가 콜백 함수를 실행한다.
[ useEffect로 라이프 사이클 제어 ]
useEffect를 이용하여 컴포넌트의 라이프 사이클 중 업데이트(Update)가 발생하면 특정 코드를 실행할 수 있다. App 컴포넌트를 다음과 같이 수정한다.
(...)
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleSetCount = (value) => {
setCount(count + value);
};
const handleChangeText = (e) => {
setText(e.target.value);
};
useEffect(() => {
console.log("컴포넌트 업데이트");
});
(..)
}
export default App;
의존성 배열에 아무것도 전달하지 않으면 useEffect는 컴포넌트를 렌더링할 때마다 콜백 함수를 실행하게 된다.
[ 리렌더 될 때만 콜백 함수를 실행시키기 ]
마운트(Mount) 시점 외에 업데이트(Update) 시점에만 콜백 함수를 실행하도록 App 컴포넌트를 다음과 같이 수정한다.
import { useRef, useState, useEffect } from "react";
(...)
function App() {
(...)
const didMountRef = useRef(false);
useEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true;
return;
} else {
console.log("컴포넌트 업데이트!");
}
});
(...)
}
export default App;
여기서 useEffect의 콜백 함수에 추가한 조건문을 자세히 살펴보자.
useEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true;
return;
} else {
console.log("컴포넌트 업데이트!");
}
});
useEffect에서 의존성 배열을 인수로 전달하지 않으면 마운트, 업데이트 시점 모두 콜백 함수를 호출한다. 그러나 이 조건문을 추가하면 마운트 시점(didMountRef=false)에 함수를 호출하면 아무것도 출력하지 않고 함수를 종료하고, 업데이트 시점(didMountRef=true)에 함수를 호출하면 문자열을 콘솔에 출력한다.
[ 컴포넌트의 마운트 제어 ]
컴포넌트의 마운트(Mount) 시점에 특정 기능을 실행하기 위해 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;
useEffect에서 의존성 배열에 빈 배열을 전달하면 컴포넌트의 마운트 시점에만 콜백 함수를 실행한다.
[ 컴포넌트 언마운트 제어 ]
리액트의 클린업(Cleanup) 기능은 특정 함수가 실행되고 종료된 후에, 미처 정리하지 못한 사항을 처리한다.
클린업 기능을 이해하기 위해서 App 컴포넌트에서 함수 useEffect를 한 번 더 호출하도록 수정한다.
(...)
function App() {
(...)
useEffect(() => {
setInterval(() => {
console.log("깜빡");
}, 1000);
});
(...)
}
export default App;
개발자 콘솔을 통해 테스트하면 함수 setInterval에서 정한 인터벌(1초)이 아닌 속도로 출력될 수 있는데, 이유는 무엇일까?
첫째로, App 컴포넌트를 렌더링할 때마다 useEffect의 콜백 함수는 새로운 setInterval 함수를 만들고 새 인터벌 간격을 생성하기 때문이다. 둘째로는 기존 인터벌을 종료하지 않았기 때문에 여러 개의 인터벌이 중복으로 만들어지기 때문이기도 하다.
[ useEffect의 클린업 기능 사용하기 ]
앞서 작성한 useEffect 코드를 다음과 같이 수정한다.
(...)
function App() {
(...)
useEffect(() => {
const intervalID = setInterval(() => {
console.log("깜빡");
}, 1000);
return () => {
console.log("클린업");
clearInterval(intervalID);
};
});
(...)
}
export default App;
useEffect의 콜백 함수가 반환하는 함수를 클린업 함수라고 합니다. 이 함수는 콜백 함수를 다시 호출하기 전에 실행되므로 이 렌더링할 때마다 새 인터벌을 생성하고 기존 인터벌은 삭제한다.
⚠️지금 작성한 useEffect는 모두 삭제 또는 주석 처리하여야 다음 실습을 진행할 수 있다.
[ 클린업을 이용해 컴포넌트 언마운트 제어 ]
'컴포넌트 언마운트'란 클린업 기능을 이용하여 컴포넌트가 페이지에서 사라질 때 원하는 코드를 실행할 수 있는 기능이다. 새 컴포넌트를 만들기 위해 component 폴더 내에 Even.js를 생성하고 다음과 같이 작성한다.
function Even() {
return <div>현재 카운트는 짝수입니다</div>;
}
export default Even;
count 값이 짝수일 때, 컴포넌트를 페이지에 렌더링하도록 App.js를 수정한다.
(...)
import Even from "./component/Even"; ①
function App() {
(...)
return (
<div className="App">
(...)
<section>
<Viewer count={count} />
{count % 2 === 0 && <Even />} ②
</section>
(...)
</div>
);
}
export default App;
이 코드에서는 ②에서 AND 단축 평가를 이용하여 조건부 렌더링 코드를 간결하게 작성하였다. AND 연산자 앞의 식이 참이면 연산자 뒤의 Even 컴포넌트를 값으로 반환한다.
[ 컴포넌트가 언마운트될 때 콘솔에 특정 문자열을 출력하게 해 보자 ]
Even 컴포넌트에서 useEffect를 이용하여 언마운트 시점에 콘솔에 특정 문자열을 출력하도록 하자. Even.js를 다음과 같이 수정한다.
import { useEffect } from "react";
function Even() {
useEffect(() => {
return () => {
console.log("Even 컴포넌트 언마운트");
};
}, []);
return <div>현재 카운트는 짝수입니다</div>;
}
export default Even;
함수 useEffect에 의존성 배열로 빈 배열을 전달하고, 콜백 함수가 함수를 반환하면 이 함수는 컴포넌트의 언마운트 시점에 실행된다.
개발자 콘솔을 이용하여 State 값이 홀수가 되면, Even 컴포넌트를 언마운트하면서 콘솔에 'Even 컴포넌트 언마운트'라는 문자열을 출력하는 것을 확인할 수 있다.
리액트 훅의 하나인 함수 useEffect를 이용하면 값을 검사하고, 컴포넌트 라이프 사이클을 제어할 수 있다.
useEffect 를 수정하고 console.log로 확인하는 과정의 번거로움을 해결하기 위해 리액트 개발자 도구를 사용할 수 있다.
[ 리액트 개발자 도구 설치하기 ]
먼저 구글 홈페이지를 통해 Chrome 웹 스토어에 접속한다. https://chromewebstore.google.com/category/extensions
React Developer Tools 프로그램을 설치한다. 권한 경고 창이 뜨는 경우 <확장 프로그램 추가> 또는 <권한 모두 허용> 버튼을 클릭 후 설치한다.
[도구 더보기]-[확장 프로그램]을 클릭하고 React Developer Tools 스위치를 On으로 설정한 뒤 <세부 정보> 페이지에서 옵션을 다음과 같이 설정한다.
마지막으로 확장 프로그램 아이콘을 클릭해 고정하면 크롬 우측 상단에 아이콘과 상태가 표시된다.
[ 설치 확인하기 ]
VS Code에서 카운터 앱을 npm run start 명령어로 시작하면 리액트 개발자 도구 아이콘이 나타나는 것을 확인할 수 있다.
[ 리액트 개발자 도구의 기능 사용하기 ]
먼저 컴포넌트 트리를 사용하기 위해 도구의 [Components] 탭을 이용한다.
루트 컴포넌트인 App 아래에 Viewer, Even, Controller 3개의 자식 컴포넌트가 컴포넌트 트리를 구성하고 있는 것을 확인할 수 있다. App를 클릭하면 Props와 State 상태를 즉각적으로 모니터링이 가능하다. State, Ref, Effect 등 리액트의 기능들도 가시적으로 표시된다.
다음은 값이 계속 변경되는 State를 모니터링하는 과정이다. [Components] 탭에서 App를 클릭해 첫 번째 State 값을 확인한다.
App 컴포넌트의 State 변수 count 값이 0에서 10으로 변경된 것을 확인할 수 있다.
다음은 Viewer 컴포넌트를 통해 Props를 모니터링하는 과정이다. [Components] 탭에서 Viewer 컴포넌트를 클릭한다.
Viewer 컴포넌트가 Props로 받은 값 10이 리액트 개발자 도구의 [Components] 탭에도 잘 표시되고 있는 것을 확인할 수 있다.
리렌더가 발생한 컴포넌트를 하이라이트할 수도 있다. [Components] 탭 옆의 View Setting 아이콘을 클릭한 뒤 창 [General] 탭에서 “Highlight updates when component render.” 항목을 체크한다.
예시 이미지와 같이 카운터 State가 업데이트될 때 컴포넌트가 리렌더링되면서 초록색에서 노란색으로 변한다. 노란색에 가까울수록 짧은 시간 내 리렌더링이 빈번하게 발생했음을 나타낸다.
1. (____)는 사용자 인터페이스라는 뜻으로, 웹 페이지에서 사용자와 상호작용하는 요소이다.
2. (____) 기능은 특정 함수가 실행되고 종료된 후에, 미처 정리하지 못한 사항을 처리하는 일을 말한다.
3. State를 변경하는 이벤트는 부모에서 자식을 향해 단방향으로 전달한다. ( O / X )
4. 리액트 컴포넌트의 라이프 사이클 3단계는 (____) , (____) , (____)로 구분한다.
5. 함수 (____)는 어떤 값이 변경될 때마다 특정 코드를 실행하는 리액트 훅이다.
6. (____)는 이 배열 요소의 값이 변경되면 첫 번째 인수로 전달한 (____)를 실행한다.
7. State 값이나 set함수를 여러 컴포넌트에서 사용하는 경우, 이들을 상위 컴포넌트에서 관리하는 기능을 (____)라고 한다.
※ 코드 작성 문제
1. App.js 코드를 참고하여 Controller 컴포넌트가 +1, +5, -1, -5 4개의 버튼을 렌더링하도록 Controller.js 코드를 작성하시오.
import Controller from "./components/Controller";
function App() {
return (
<div className="App">
<header>
<h1>카운터 컨트롤러</h1>
</header>
<main>
<Controller />
</main>
</div>
);
}
export default App;
2. 다음 코드는 setInterval에서 정한 인터벌(5초)이 아닌 속도로 출력될 수 있다. useEffect의 콜백 함수를 이용하여 정상적인 속도로 출력될 수 있도록 코드를 수정하여라.
(...)
function App() {
(...)
useEffect(() => {
setInterval(() => {
console.log("5초");
}, 5000);
});
(...)
}
export default App;
1. ui / 2. 클린업 기능 / X ( 자식에서 부모를 향해 역방향으로 전달되어야 한다 ) / 4. 마운트, 업데이트, 언마운트 / 5. useEffect / 6. useEffect,콜백 함수 / 7. State 끌어올리기
서술형 1.
const Controller = () => {
return (
<div>
<button>+1</button>
<button>+5</button>
<button>-1</button>
<button>-5</button>
</div>
);
};
export default Controller;
서술형 2.
(...)
function App() {
(...)
useEffect(() => {
setInterval(() => {
console.log("5초");
}, 5000);
return () => {
console.log("클린업");
clearInterval(intervalID);
};
});
(...)
}
export default App;
출처: 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023), https://reactjs.winterlood.com/.
Corner React.js 2
Editor: Chacha
[React.js 2팀] project 2 [할 일 관리] 앱 만들기 1 (프로젝트 준비하기 ~ Create: 할 일 추가하기) (1) | 2024.12.27 |
---|---|
[React.js 2팀] 8장. Hooks (0) | 2024.11.29 |
[React.js 2팀] 5장. 리액트의 기본 기능 다루기 (2) (1) | 2024.11.15 |
[React.js 2팀] 5장. 리액트의 기본 기능 다루기 (1) (0) | 2024.11.08 |
[React.js 2팀] 3장. Node.js ~ 4장. 리액트 시작하기 (10) | 2024.10.11 |