React 상호작용: 편집하기, 필터링, 조건부 렌더링
React 여정이 거의 끝나감에 따라(적어도 지금은), Todo 목록 애플리케이션의 주요 기능 영역에 마무리 작업을 추가할 것입니다. 여기에는 기존 할 일을 편집할 수 있도록 허용하고, 모든 할 일, 완료된 할 일 및 완료되지 않은 할 일 간의 할 일 목록을 필터링하는 것이 포함됩니다. 그 과정에서 조건부 UI 렌더링을 살펴보겠습니다.
필요한 사전 지식: |
HTML, CSS, 및 JavaScript 언어 핵심에 대한 친숙도, 터미널/커멘드 라인에 대한 지식. |
---|---|
목표: | React의 조건부 렌더링에 대해 배우고 애플리케이션에서 목록 필터링 및 편집 UI를 구현합니다. |
할 일 이름 편집
아직 할 일 이름을 편집하기 위한 사용자 인터페이스가 없습니다. 우리는 잠시 후에 그것을 얻을 것입니다. 우선 App.js
에서 적어도 editTask()
함수를 구현할 수 있습니다. 대상 객체를 찾기 위해 id
를 사용하기 때문에, deleteTask()
와 비슷하지만 할 일을 업데이트할 이름이 포함된 newName
속성도 사용합니다. 배열에서 무언가를 삭제하는 대신, 약간의 변경사항이 있는 새 배열을 반환하고 싶기 때문에 Array.prototype.filter()
대신 Array.prototype.map()
을 사용합니다.
App
컴포넌트 내부의 editTask()
함수를 다른 함수와 동일한 위치에 추가합니다.
function editTask(id, newName) {
const editedTaskList = tasks.map((task) => {
// 이 할 일이 편집된 작업과 동일한 ID를 갖는 경우
if (id === task.id) {
//
return { ...task, name: newName };
}
return task;
});
setTasks(editedTaskList);
}
deleteTask
와 같은 방식으로 editTask
를 <Todo />
컴포넌트에 속성으로 전달합니다.
const taskList = tasks.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
toggleTaskCompleted={toggleTaskCompleted}
deleteTask={deleteTask}
editTask={editTask}
/>
));
이제 Todo.js
를 엽니다. 우리는 약간의 리팩토링을 할 것입니다.
편집을 위한 UI
사용자가 할 일을 편집할 수 있도록 하려면, 그렇게 할 수 있는 사용자 인터페이스를 제공해야 합니다. 먼저 App
컴포넌트에서 이전에 했던 것처럼 useState
를 Todo
컴포넌트로 가져옵니다. 첫 번째 import 문을 다음과 같이 업데이트합니다.
import React, { useState } from "react";
이제 이것을 사용하여 isEditing
상태를 설정하고, 기본 상태는 false
여야 합니다. Todo(props) {}
컴포넌트 정의의 맨 위에 다음 행을 추가하십시오.
const [isEditing, setEditing] = useState(false);
다음으로, <Todo />
컴포넌트를 다시 생각해 보겠습니다. 이제부터 지금까지 사용된 단일 템플릿이 아닌, 두 가지 가능한 "템플릿" 중 하나를 표시하려고 합니다.
- "보기" 템플릿, 단지 할 일을 볼 때, 이것이 지금까지 튜토리얼에서 사용한 것입니다.
- 할 일을 편집할 때, "편집" 템플릿입니다. 우리는 이것을 만들려고 합니다.
이 코드 블록을 useState()
훅 아래, return
문 위에 있는 Todo()
함수에 복사합니다.
const editingTemplate = (
<form className="stack-small">
<div className="form-group">
<label className="todo-label" htmlFor={props.id}>
{props.name}의 새로운 이름
</label>
<input id={props.id} className="todo-text" type="text" />
</div>
<div className="btn-group">
<button type="button" className="btn todo-cancel">
취소
<span className="visually-hidden">{props.name} 이름 바꾸기</span>
</button>
<button type="submit" className="btn btn__primary todo-edit">
저장
<span className="visually-hidden">{props.name}의 새로운 이름</span>
</button>
</div>
</form>
);
const viewTemplate = (
<div className="stack-small">
<div className="c-cb">
<input
id={props.id}
type="checkbox"
defaultChecked={props.completed}
onChange={() => props.toggleTaskCompleted(props.id)}
/>
<label className="todo-label" htmlFor={props.id}>
{props.name}
</label>
</div>
<div className="btn-group">
<button type="button" className="btn">
편집 <span className="visually-hidden">{props.name}</span>
</button>
<button
type="button"
className="btn btn__danger"
onClick={() => props.deleteTask(props.id)}>
삭제 <span className="visually-hidden">{props.name}</span>
</button>
</div>
</div>
);
이제 두 개의 서로 다른 상수 내에 정의된, "편집" 및 "보기"라는 두 가지 템플릿 구조가 있습니다. 이는 <Todo />
의 return
문이 이제 반복된다는 것을 의미합니다. 또한 "보기" 템플릿의 정의도 포함됩니다. 컴포넌트가 반환하는 템플릿을 결정하기 위해 조건부 렌더링을 사용하여 이를 정리할 수 있으므로, UI에서 렌더링됩니다.
조건부 렌더링
JSX에서는, 조건을 사용하여 브라우저에서 렌더링되는 내용을 변경할 수 있습니다. JSX에서 조건을 작성하려면, 삼항 연산자를 사용할 수 있습니다.
<Todo />
컴포넌트의 경우, 우리의 조건은 "이 할 일이 편집되고 있는가?"입니다. Todo()
내부의 return
문을 다음과 같이 변경합니다.
return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>;
브라우저는 이전과 마찬가지로 모든 할 일들을 렌더링해야 합니다. 편집 템플릿을 보려면, 현재 코드에서 기본 isEditing
상태를 false
에서 true
로 변경해야 합니다. 다음 섹션에서 편집 버튼 토글을 만드는 방법을 살펴보겠습니다!
<Todo />
템플릿 토글
마침내, 최종 핵심 기능을 대화형으로 만들 준비가 되었습니다. 우선, 사용자가 viewTemplate
에서 "편집" 버튼을 누를 때 값이 true
인 setEditing()
을 호출하여 템플릿을 전환할 수 있도록 합니다.
다음과 같이 viewTemplate
에서 "편집" 버튼을 업데이트합니다.
<button type="button" className="btn" onClick={() => setEditing(true)}>
편집 <span className="visually-hidden">{props.name}</span>
</button>
이제 동일한 onClick
핸들러를 editingTemplate
의 "Cancel" 버튼에 추가하지만, 이번에는 isEditing
을 false
로 설정하여 "보기" 템플릿으로 다시 전환합니다.
다음과 같이 editingTemplate
에서 "취소" 버튼을 업데이트합니다.
<button
type="button"
className="btn todo-cancel"
onClick={() => setEditing(false)}>
취소
<span className="visually-hidden">{props.name} 이름 바꾸기</span>
</button>
이 코드를 사용하면, 할 일 항목에서 "편집" 및 "취소" 버튼을 눌러 템플릿 간에 전환할 수 있어야 합니다.
다음 단계는 실제로 편집 기능을 작동시키는 것입니다.
UI에서 편집하기
우리가 하려는 많은 작업은 Form.js
에서 수행한 작업을 반영할 것입니다. 사용자가 새 입력 필드에 입력할 때 입력하는 텍스트를 추적해야 합니다. 양식을 제출하면 콜백 속성을 사용하여 할 일의 새 이름으로 상태를 업데이트해야 합니다.
새 이름을 저장하고 설정하기 위한 새 훅을 만드는 것으로 시작하겠습니다. 여전히 Todo.js
에서 기존 훅 아래에 다음을 추가합니다.
const [newName, setNewName] = useState("");
다음으로, 새 이름을 설정할 handleChange()
함수를 만듭니다. 이 함수를 훅과 템플릿의 사이에 넣으세요.
function handleChange(e) {
setNewName(e.target.value);
}
이제 editingTemplate
의 <input />
필드를 업데이트하고 value
속성을 newName
으로 설정하고, handleChange()
함수를 onChange
이벤트에 바인딩합니다.
<input
id={props.id}
className="todo-text"
type="text"
value={newName}
onChange={handleChange}
/>
마지막으로, 편집 양식의 onSubmit
이벤트를 처리하는 함수를 만들어야 합니다. 이전에 추가한 함수 바로 아래에 다음을 추가합니다.
function handleSubmit(e) {
e.preventDefault();
props.editTask(props.id, newName);
setNewName("");
setEditing(false);
}
editTask()
콜백 속성에는 편집 중인 할 일의 ID와 새 이름이 필요하다는 점을 기억하세요.
다음 onSubmit
핸들러를 editingTemplate
의 <form>
에 추가하여 이 함수를 양식의 submit
이벤트에 바인딩합니다.
<form className="stack-small" onSubmit={handleSubmit}>
이제 브라우저에서 할 일을 편집할 수 있습니다!
필터 버튼으로 돌아가기
이제 주요 기능이 완성되었으니, 필터 버튼에 대해 생각해볼 수 있습니다. 현재, 필터 버튼은 "All" 레이블을 반복하며, 아무런 기능도 없습니다! 여기에는 <Todo />
컴포넌트에서 사용했던 몇 가지 기술을 다시 적용할 것입니다.
- 활성된 필터를 저장하기 위한 훅을 만듭니다.
- 사용자가 활성 필터를 모두, 완료됨, 완료되지 않음으로 변경할 수 있는
<FilterButton />
요소의 배열을 렌더링합니다.
필터 훅 추가하기
필터를 읽고 설정하는 App()
함수에 새 훅을 추가합니다. 모든 할 일이 처음에 표시되어야 하므로 기본 필터를 All
로 지정합니다.
const [filter, setFilter] = useState("All");
필터 정의
현재 우리의 목표는 두 가지입니다.
- 각 필터의 이름은 고유해야 합니다.
- 각 필터에는 고유한 행위가 있어야 합니다.
JavaScript 객체는 이름과 행위를 연결하는 좋은 방법입니다. 각 키는 필터의 이름입니다. 각 속성은 해당 이름과 관련된 행위입니다.
App.js
상단에, import 문들 아래에 있지만 App()
함수 위에, FILTER_MAP
이라는 객체를 추가해보겠습니다.
const FILTER_MAP = {
All: () => true,
Active: (task) => !task.completed,
Completed: (task) => task.completed,
};
FILTER_MAP
의 값은 tasks
데이터 배열을 필터링하는 데 사용할 함수입니다.
All
필터는 모든 할 일을 표시하므로, 모든 할 일에 대해true
를 반환합니다.Active
필터는completed
속성이false
인 할 일을 표시합니다.Completed
필터는completed
속성이true
인 할 일을 표시합니다.
이전에 추가된 항목 아래에, 다음을 추가합니다. 여기서는 Object.keys()
를 사용하여 FILTER_NAMES
배열을 수집합니다.
const FILTER_NAMES = Object.keys(FILTER_MAP);
참고 :
우리는 App()
함수 외부에서 이러한 상수들을 정의하고 있습니다. 내부에 정의된 경우, <App />
컴포넌트가 다시 렌더링될 때마다 다시 계산되기 때문인데, 우리는 그것을 원하지 않습니다. 이 정보는 애플리케이션이 무엇을 하든 절대 변경되지 않습니다.
필터 렌더링
이제 FILTER_NAMES
배열이 있으므로, 이를 사용하여 세 가지 필터를 모두 렌더링할 수 있습니다. App()
함수 내에서 filterList
라는 상수를 만들 수 있습니다. 이 상수는 이름 배열을 매핑하고 <FilterButton />
컴포넌트를 반환하는 데 사용할 것입니다. 여기서도 키가 필요하다는 것을 기억하세요.
taskList
상수 선언 아래에 다음을 추가합니다.
const filterList = FILTER_NAMES.map((name) => (
<FilterButton key={name} name={name} />
));
이제 App.js
에서 반복되는 세 개의 <FilterButton />
을 이 filterList
로 바꾸어줍니다. 다음을 변경해주세요.
<FilterButton />
<FilterButton />
<FilterButton />
이 코드로 변경해주세요.
{filterList}
아직 작동하지 않습니다. 먼저 해야 할 일이 조금 더 있습니다.
상호 작용을 하는 필터
필터 버튼을 상호 작용할 수 있게 만들려면, 어떤 속성을 활용해야 하는지 고려해야 합니다.
- 우리는
<FilterButton />
이 현재 눌렸는지 여부를 보고해야 하며, 이름이 필터 상태의 현재 값과 일치하면 눌러야 한다는 것을 알고 있습니다. - 활성 필터를 설정하려면
<FilterButton />
에 콜백이 필요하다는 것을 알고 있습니다.setFilter
훅을 직접 사용할 수 있습니다.
다음과 같이 filterList
상수를 업데이트합니다.
const filterList = FILTER_NAMES.map((name) => (
<FilterButton
key={name}
name={name}
isPressed={name === filter}
setFilter={setFilter}
/>
));
이전에 <Todo />
컴포넌트에서 했던 것과 같은 방식으로, 이제 FilterButton.js
를 업데이트하여 제공된 속성을 활용해야 합니다. 다음을 각각 수행하고, 중괄호를 사용하여 이러한 변수를 읽어오는 것을 잊지 마세요!
all
을{props.name}
으로 바꾸어줍니다.aria-pressed
값을{props.isPressed}
로 설정합니다.- 필터 이름으로
props.setFilter()
를 호출하는onClick
핸들러를 추가합니다.
모든 할 일이 완료되면, FilterButton()
함수는 다음과 같아야 합니다.
function FilterButton(props) {
return (
<button
type="button"
className="btn toggle-btn"
aria-pressed={props.isPressed}
onClick={() => props.setFilter(props.name)}>
<span className="visually-hidden">보여주다</span>
<span>{props.name}</span>
<span className="visually-hidden">할 일들</span>
</button>
);
}
브라우저를 다시 열어보세요. 서로 다른 버튼에 각각의 이름이 부여된 것을 볼 수 있습니다. 필터 버튼을 누르면, 해당 텍스트가 새로운 윤곽선에 나타나는 것을 볼 수 있습니다. 이는 해당 버튼이 선택되었음을 알려줍니다. 버튼을 클릭하는 동안 DevTool의 Page Inspector를 보면, 그에 따라 aria-pressed
속성 값이 변경되는 것을 볼 수 있습니다.
그러나, 버튼은 여전히 UI에서 할 일을 실제로 필터링하지 않습니다! 이것으로 마무리하겠습니다.
UI에서 할 일 필터링
지금은, App()
의 taskList
상수가 할 일 상태를 매핑하고 모든 할 일에 대한 새로운 <Todo />
컴포넌트를 반환합니다. 이것은 우리가 원하는 것이 아닙니다! 할 일은 선택한 필터를 적용한 결과에 포함된 경우에만 렌더링되어야 합니다. 할 일 상태를 매핑하기 전에, 렌더링하지 않으려는 객체를 제거하기 위해 필터링(Array.prototype.filter()
)해야 합니다.
다음과 같이 taskList
를 업데이트하세요.
const taskList = tasks
.filter(FILTER_MAP[filter])
.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
toggleTaskCompleted={toggleTaskCompleted}
deleteTask={deleteTask}
editTask={editTask}
/>
));
Array.prototype.filter()
에서 사용할 콜백 함수를 결정하기 위해, 필터 상태의 키에 해당하는 FILTER_MAP
의 값에 접근합니다. 예를 들어, 필터가 All
인 경우, FILTER_MAP[filter]
는 () => true
로 평가됩니다.
이제 브라우저에서 필터를 선택하면 기준에 맞지 않는 할 일들이 제거됩니다. 목록 위의 제목에 있는 개수도 목록을 반영하도록 변경됩니다.