React 앱 컴포넌트화
이 시점에서 우리의 앱은 모놀리식입니다. 우리는 이를 작동시키기 전에, 관리 가능하고, 기술적인 컴포넌트로 나눌 필요가 있습니다. React는 컴포넌트로 무엇이고 무엇이 아닌지에 대한 엄격한 규칙은 없으며, 당신에게 달려 있습니다! 이 글에서는 우리의 앱을 컴포넌트로 나누는 합리적인 방법을 보여줄 것입니다.
| 필요한 사전 지식: |
코어 HTML, CSS, 및 JavaScript 언어에 익숙할 것. 터미널/커맨드라인에 대한 지식. |
|---|---|
| 목표: | 할 일 목록 앱을 컴포넌트로 나누는 합리적인 방법을 보여주고자 합니다. |
첫 번째 컴포넌트 정의하기
컴포넌트를 정의하는 것은 연습이 필요할 때까지는 까다로워 보일 수 있지만, 요점은 다음과 같습니다.
- 만약 당신의 앱의 명확한 "조각"을 나타낸다면, 아마도 컴포넌트일 것입니다.
- 만약 자주 재사용된다면, 아마도 컴포넌트일 것입니다.
특히 두번째 항목은 중요합니다. 자주 재사용되는 UI 요소를 컴포넌트로 만들면, 코드를 한 곳에서 변경하고 그 컴포넌트가 사용되는 모든 곳에서 변경 사항을 볼 수 있습니다. 모든 것을 컴포넌트로 분리할 필요는 없습니다. 두번째 항목을 영감으로 삼아, 가장 재사용되고 중요한 UI 조각인 todo list의 항목을 컴포넌트로 만들어 보겠습니다.
<Todo /> 만들기
컴포넌트를 만들기 전에, 새로운 파일을 만들어야 합니다. 사실, 우리는 컴포넌트를 위한 디렉토리를 만들어야 합니다. 다음 명령어는 components 디렉토리를 만들고, 그 안에 Todo.js라는 파일을 만듭니다. 이 명령어를 실행하기 전에, 당신이 앱의 루트에 있는지 확인하세요!
mkdir src/components
touch src/components/Todo.js
Todo.js 파일은 현재 비어있습니다! 열어서 다음의 첫 번째 줄을 추가하세요.
import React from "react";
Todo라는 컴포넌트를 만들 것이므로, 다음과 같이 Todo.js에 코드를 추가할 수 있습니다. 이 코드에서, 우리는 함수를 다음과 같이 정의하고 내보냅니다.
function Todo() {
return (
// …
);
}
export default Todo;
지금까지는 괜찮지만, 우리의 컴포넌트는 무언가를 반환해야 합니다! src/App.js로 돌아가서, 정렬되지 않은 목록 안에 있는 첫 번째 <li>를 복사하고, Todo.js에 붙여넣어서 다음과 같이 만드세요.
export default function Todo() {
return (
<li className="todo stack-small">
<div className="c-cb">
<input id="todo-0" type="checkbox" defaultChecked={true} />
<label className="todo-label" htmlFor="todo-0">
Eat
</label>
</div>
<div className="btn-group">
<button type="button" className="btn">
Edit <span className="visually-hidden">Eat</span>
</button>
<button type="button" className="btn btn__danger">
Delete <span className="visually-hidden">Eat</span>
</button>
</div>
</li>
);
}
참고 : 컴포넌트는 항상 무언가를 반환해야 합니다. 만약 앞으로 컴포넌트를 렌더링하려고 할 때, 아무것도 반환하지 않는 컴포넌트를 만든다면, React는 브라우저에 에러를 표시할 것입니다.
우리가 만든 Todo 컴포넌트가 완성되어 사용할 수 있습니다. App.js에 다음과 같이 Todo를 가져오는 코드를 파일의 상단에 추가하세요.
import Todo from "./components/Todo";
해당 컴포넌트를 가져왔다면, App.js의 모든 <li> 요소를 <Todo /> 컴포넌트로 바꿀 수 있습니다. <ul>는 다음과 같이 읽을 수 있습니다.
<ul
role="list"
className="todo-list stack-large stack-exception"
aria-labelledby="list-heading">
<Todo />
<Todo />
<Todo />
</ul>
브라우저를 새로고침하면, 불행히도 첫 번째 할 일이 세 번 반복되는 것을 알 수 있습니다.

우리는 먹는 것만 하고 싶지 않습니다. 다른 할 일도 있습니다. 다음에는 컴포넌트를 호출할 때 고유한 내용을 렌더링하는 방법을 살펴보겠습니다.
고유한 <Todo /> 만들기
컴포넌트는 UI의 일부를 재사용하고, UI의 소스를 하나의 장소로 참조할 수 있기 때문에 강력합니다. 문제는, 우리는 일반적으로 모든 컴포넌트를 재사용하고 싶지 않습니다. 대부분의 부분을 재사용하고, 작은 부분을 변경하고 싶습니다. 이것이 props가 필요한 이유입니다.
name에는 무엇이 들어갈까요?
할 일의 이름을 추적하기 위해서, 각 <Todo /> 컴포넌트가 고유한 이름을 렌더링하도록 해야 합니다.
App.js에서 각 <Todo />에 name prop을 추가하세요. 이전에 사용했던 할 일의 이름을 사용해보겠습니다.
<Todo name="Eat" />
<Todo name="Sleep" />
<Todo name="Repeat" />
브라우저를 새로고침하면, 이전과 똑같은 것을 볼 수 있습니다. <Todo />에 props를 추가했지만, 아직 사용하지 않았습니다. Todo.js로 돌아가서 이 문제를 해결해보겠습니다.
먼저, Todo() 함수 정의를 수정해서 props를 매개변수로 받도록 하세요. 이전에 했던 것처럼 props를 console.log()로 출력해보면, 컴포넌트가 props를 올바르게 받고 있는지 확인할 수 있습니다.
컴포넌트가 props를 받고 있다고 확신이 들면, Eat을 name prop으로 바꿀 수 있습니다. JSX 표현식 안에서 변수의 값을 주입할 때는 중괄호를 사용한다는 것을 기억하세요.
위의 내용을 모두 합치면, Todo() 함수는 다음과 같이 읽을 수 있습니다.
function Todo(props) {
return (
<li className="todo stack-small">
<div className="c-cb">
<input id="todo-0" type="checkbox" defaultChecked={true} />
<label className="todo-label" htmlFor="todo-0">
{props.name}
</label>
</div>
<div className="btn-group">
<button type="button" className="btn">
Edit <span className="visually-hidden">{props.name}</span>
</button>
<button type="button" className="btn btn__danger">
Delete <span className="visually-hidden">{props.name}</span>
</button>
</div>
</li>
);
}
export default Todo;
"이제" 브라우저에서 세 가지 고유한 할 일을 볼 수 있습니다. 하지만, 여전히 모두 기본적으로 체크되어 있습니다.

completed인가요?
기존의 정적인 목록에서는 Eat만 체크되어 있었습니다. 다시 말하지만, <Todo /> 컴포넌트를 구성하는 UI의 대부분을 재사용하고 싶지만, 한 가지를 바꾸고 싶습니다. 그것은 또 다른 prop을 위한 좋은 작업입니다! App.js에서 각 <Todo /> 호출에 completed라는 새로운 prop을 추가하세요. 첫 번째(Eat)는 true의 값을 가져야 하고, 나머지는 false여야 합니다.
<Todo name="Eat" completed={true} />
<Todo name="Sleep" completed={false} />
<Todo name="Repeat" completed={false} />
이전과 마찬가지로, 이제 Todo.js로 돌아가서 이 props를 사용해야 합니다. <input />의 defaultChecked 속성을 completed prop과 동일한 값으로 변경하세요. 변경이 완료되면, Todo 컴포넌트의 <input /> 요소는 다음과 같이 읽을 수 있습니다.
<input id="todo-0" type="checkbox" defaultChecked={props.completed} />
그러면 다음과 같이 브라우저에서 Eat만 체크된 것을 볼 수 있습니다.

<Todo /> 컴포넌트의 completed prop을 변경하면, 브라우저에서 렌더링된 체크박스가 그에 상응하는 체크 또는 체크 해제됩니다.
id를 부여해주세요
지금은 <Todo /> 컴포넌트가 모든 할 일에 id 속성을 todo-0으로 부여합니다. 이것은 id 속성은 고유해야 한다는 점에서 나쁜 HTML입니다. (CSS, JavaScript 등에서 문서 조각을 식별하는 데 사용됩니다.) 이것은 각 Todo에 대해 고유한 값을 취하는 id prop을 컴포넌트에 부여해야 한다는 것을 의미합니다.
우리가 처음에 사용한 패턴을 따라서, <Todo /> 컴포넌트의 각 인스턴스에 todo-i 형식의 ID를 부여해보겠습니다. 여기서 i는 매번 1씩 증가합니다.
<Todo name="Eat" completed={true} id="todo-0" />
<Todo name="Sleep" completed={false} id="todo-1" />
<Todo name="Repeat" completed={false} id="todo-2" />
이제 Todo.js로 돌아가서 id prop을 사용해보겠습니다. <input /> 요소의 id 속성의 값을, 그리고 레이블의 htmlFor 속성의 값을 바꿔야 합니다.
<div className="c-cb">
<input id={props.id} type="checkbox" defaultChecked={props.completed} />
<label className="todo-label" htmlFor={props.id}>
{props.name}
</label>
</div>
지금까지는 잘 되고 있나요?
React를 잘 사용하고 있지만, 더 나은 방법이 있습니다! 우리의 코드는 반복적입니다. <Todo /> 컴포넌트를 렌더링하는 세 줄은 거의 동일합니다. 하나의 차이점만 있습니다: 각 prop의 값입니다.
JavaScript의 핵심 기능 중 하나인 반복을 사용하여 코드를 정리할 수 있습니다. 반복을 사용하기 위해서는 먼저 할 일을 다시 생각해야 합니다.
할 일을 데이터로
현재 각 할 일은 이름, 체크 여부, 고유 ID라는 세 가지 정보를 포함하고 있습니다. 이 데이터는 객체로 잘 표현됩니다. 여러 개의 할 일이 있으므로, 객체의 배열이 이 데이터를 잘 표현할 수 있습니다.
src/index.js에서, 마지막 import 아래에 새로운 const를 만드세요. 하지만 ReactDOM.render() 위에 위치해야 합니다.
const DATA = [
{ id: "todo-0", name: "Eat", completed: true },
{ id: "todo-1", name: "Sleep", completed: false },
{ id: "todo-2", name: "Repeat", completed: false },
];
다음으로, DATA를 tasks라는 prop으로 <App />에 전달합니다. src/index.js의 마지막 줄은 다음과 같아야 합니다.
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App tasks={DATA} />
</React.StrictMode>,
);
이 배열은 이제 props.tasks로 <App /> 컴포넌트에서 사용할 수 있습니다. 원한다면 console.log()를 사용하여 확인할 수 있습니다.
참고 :
ALL_CAPS (모두 대문자인) 상수 이름은 JavaScript에서 특별한 의미가 없습니다. 이것은 다른 개발자들에게 "이 데이터는 여기에서 정의된 후에는 절대로 변경되지 않을 것이다"라고 알려주는 관습입니다.
반복을 사용하여 렌더링하기
객체 배열을 렌더링하려면, 각 객체를 <Todo /> 컴포넌트로 변환해야 합니다. JavaScript는 항목을 다른 것으로 변환하는 Array.prototype.map() 배열 메서드를 제공합니다.
App()의 return 문 위에 taskList라는 새 const를 만드세요. props.tasks 배열의 각 할 일을 name으로 변환하는 것부터 시작해보겠습니다.
const taskList = props.tasks?.map((task) => task.name);
<ul>의 모든 자식을 taskList로 바꿔보겠습니다.
<ul
role="list"
className="todo-list stack-large stack-exception"
aria-labelledby="list-heading">
{taskList}
</ul>
이렇게 하면 컴포넌트를 다시 표시하는 데 일부 도움이 되지만, 더 많은 작업이 필요합니다. 브라우저는 현재 각 할 일의 이름을 구조화되지 않은 텍스트로 렌더링합니다. HTML 구조인 <li>와 해당 체크박스와 버튼이 누락되었습니다!

이를 해결하려면 map() 함수에서 <Todo /> 컴포넌트를 반환해야 합니다. JSX를 사용하면 JavaScript와 마크업 구조를 혼합할 수 있습니다! 이전에 작성한 코드 대신 다음을 시도해 보겠습니다.
const taskList = props.tasks.map((task) => <Todo />);
앱을 다시 확인하세요. 이제 할 일이 이전과 비슷해 보이지만, 할 일 이름이 누락되었습니다. 매핑하는 각 할 일에는 <Todo /> 컴포넌트에 전달할 id, name, completed 속성이 있습니다. 이를 함께 사용하면 다음과 같은 코드가 됩니다.
const taskList = props.tasks.map((task) => (
<Todo id={task.id} name={task.name} completed={task.completed} />
));
이제 앱은 이전과 같이 보이며, 코드가 덜 반복됩니다.
고유 키
이제 React는 배열에서 할 일을 렌더링하므로, 올바르게 렌더링하려면 어떤 것이 어떤 것인지 추적해야 합니다. React는 추적을 위해 자체적으로 추측을 시도하지만, <Todo /> 컴포넌트에 key prop을 전달하여 도움을 줄 수 있습니다. key는 React가 관리하는 특별한 prop입니다. 다른 용도로 key라는 단어를 사용할 수 없습니다.
key는 고유해야 하므로, 각 task 객체의 id를 키로 재사용할 것입니다. taskList 상수를 다음과 같이 업데이트하세요.
const taskList = props.tasks.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
/>
));
반복문으로 렌더링하는 모든 것에 고유한 키를 전달해야 합니다. 브라우저에서 명확한 변경 사항은 없지만, 고유한 키를 사용하지 않으면 React가 콘솔에 경고를 기록하고 앱이 이상하게 동작할 수 있습니다!
앱의 나머지 부분도 컴포넌트화하기
이제 가장 중요한 컴포넌트를 정리했으므로, 앱의 나머지 부분을 컴포넌트로 바꿀 수 있습니다. 컴포넌트는 UI의 명확한 부분, 재사용되는 UI의 부분 또는 둘 다일 수 있다는 것을 기억하세요. 두 개의 컴포넌트를 더 만들 수 있습니다.
<Form/><FilterButton/>
두 컴포넌트가 필요하다는 것을 알기 때문에, 터미널 명령으로 일부 파일 생성 작업을 함께 일괄 처리할 수 있습니다. 앱의 루트 디렉터리에 있는지 확인하고 다음 명령을 터미널에서 실행하세요.
touch src/components/Form.js src/components/FilterButton.js
<Form />
components/Form.js를 열고 다음을 수행하세요.
Todo.js에서 수행한 것처럼 파일 상단에서React를 가져옵니다.Todo()와 동일한 기본 구조를 가진 새Form()컴포넌트를 만듭니다.App.js내부에서<form>태그와 그 사이의 모든 내용을 복사하여Form()의return문 내부에 붙여넣습니다.- 파일 끝에서
Form을 내보냅니다.
Form.js 파일은 다음과 같아야 합니다.
import React from "react";
function Form(props) {
return (
<form>
<h2 className="label-wrapper">
<label htmlFor="new-todo-input" className="label__lg">
무엇을 해야 합니까?
</label>
</h2>
<input
type="text"
id="new-todo-input"
className="input input__lg"
name="text"
autoComplete="off"
/>
<button type="submit" className="btn btn__primary btn__lg">
추가
</button>
</form>
);
}
export default Form;
<FilterButton />
FilterButton.js 내부에서 Form.js를 만드는 데 사용한 것과 동일한 작업을 수행합니다. 하지만, 컴포넌트를 FilterButton()으로 호출하고 App.js의 class가 filters인 <div> 요소 내부의 첫 번째 버튼에 대한 HTML을 return 문으로 복사합니다.
파일은 다음과 같이 보여야 합니다.
import React from "react";
function FilterButton(props) {
return (
<button type="button" className="btn toggle-btn" aria-pressed="true">
<span className="visually-hidden">Show </span>
<span>all </span>
<span className="visually-hidden"> tasks</span>
</button>
);
}
export default FilterButton;
참고 :
여기서 <Todo /> 컴포넌트를 처음 만들 때와 동일한 실수를 하고 있다는 것을 알 수 있습니다. 각 버튼이 동일할 것입니다. 괜찮습니다! 필터 버튼으로 돌아가기에서 이 컴포넌트를 나중에 수정할 것입니다.
모든 컴포넌트를 가져오기
그럼 지금까지 만든 새 컴포넌트들을 사용해 보겠습니다.
App.js의 맨 위에 import 문을 몇 개 더 추가하여 가져옵니다.
그런 다음 App()의 return 문을 업데이트하여 컴포넌트를 렌더링합니다. 완료하면 App.js는 다음과 같이 보일 것입니다.
import React from "react";
import Form from "./components/Form";
import FilterButton from "./components/FilterButton";
import Todo from "./components/Todo";
function App(props) {
const taskList = props.tasks.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
/>
));
return (
<div className="todoapp stack-large">
<h1>TodoMatic</h1>
<Form />
<div className="filters btn-group stack-exception">
<FilterButton />
<FilterButton />
<FilterButton />
</div>
<h2 id="list-heading">3개의 작업이 남음</h2>
<ul
role="list"
className="todo-list stack-large stack-exception"
aria-labelledby="list-heading">
{taskList}
</ul>
</div>
);
}
export default App;
이것으로, 우리는 React 앱에서 일부 상호작용을 다룰 준비가 거의 완료되었습니다!
요약
이 문서는 여기까지입니다. 앱을 컴포넌트로 잘 나누고 효율적으로 렌더링하는 방법에 대해 자세히 알아보았습니다. 이제 React에서 이벤트를 처리하는 방법을 살펴보고 상호 작용을 추가해 보겠습니다.