리액트의 동작 원리를 파악하기 위해 구현 순서를 우선적으로 정리했다.
💡구현 순서
1. 필요한 컴포넌트 생성: components 폴더 생성 후 Header.jsx, Editior.jx, List.jsx파일 생성
2. 컴포넌트 호출: App.jsx로 들어가 생성한 컴포넌트를 불러온다.
import './App.css';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
3. 화면 렌더링: App 컴포넌트안에 각각 컴포넌트들을 렌더링한다.
function App() {
return (
<>
<Header />
<Editor />
<List />
</>
);
}
4. Header 컴포넌트 구현
import './Header.css';
import React from 'react';
const Header = () => {
return (
<div className="Header">
<h3>오늘은 👇🏻 </h3>
<h1>{new Date().toDateString()}</h1>
</div>
);
};
export default Header;
<h1>{new Date().toDateString()}</h1>
new Data → 새로운 객체 생성
toDateString() → 날짜 문자열로 변환하는 메서드 호출
5. Editor 컴포넌트 구현
import React from 'react';
import './Editor.css';
const Editor = () => {
return (
<div className="Editor">
<input placeholder="할 일을 추가해보세요."></input>
<button>추가</button>
</div>
);
};
export default Editor;
6. List 컴포넌트 구현
import React from 'react';
import './List.css';
const List = () => {
return (
<div className="List">
<h4>TODO LIST</h4>
<input placeholder="검색어를 입력하세요."></input>
</div>
);
};
export default List;
7. ToDo Item 체크 박스 구현하기
공통된 UI는 Porps( [URECA] Day 46 React 컴포넌트 — 오늘도 한줄 )는 바꿔가면서 렌더링시켜준다.
import React from 'react';
const ToDoItem = () => {
return <div>ToDoItem</div>;
};
export default ToDoItem;
임시로 ToDoItem.jsx를 만든 다음
List.jsx에 import 해준다.
import React from 'react';
import ToDoItem from './ToDoItem';
import './List.css';
const List = () => {
return (
<div className="List">
<h4>TODO LIST</h4>
<input placeholder="검색어를 입력하세요."></input>
<div>
<ToDoItem />
</div>
</div>
);
};
export default List;
ToDoItem.jsx로 돌아가 필요한 구성요소 만들기
import React from 'react';
import './ToDoItem.css';
const ToDoItem = () => {
return (
<div className="ToDoItem">
<input type="checkbox" />
<div className="content">todo</div>
<div className="date">Date</div>
<button>삭제</button>
</div>
);
};
export default ToDoItem;
ToDoItem.jsx 디자인까지 마쳤으면 기능을 구현해준다.
8. ToDo Item 기능 구현
새로운 투두를 입력해서 추가하고, 체크하거나 삭제 기능까지 구현하려면 검색 기능도 가능해야한다.
그러면 todoItemData를 state로 관리해야 한다.
그렇다면 이 state는 어디에 만들어야 할까?
(state 설명: [URECA] Day 46 React 컴포넌트 — 오늘도 한줄 )
바로 최상위 컴포넌트인 App 컴포넌트에 만들어야한다.
그래야 하위 컴포넌트에 props로 전달하면서 전체적인 상태 관리를 할 수 있다.
import './App.css';
import { useState } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
// 임시 데이터 생성
const mockData = [
{
id: 0,
isDone: false,
content: 'React 공부하기',
date: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: 'React 뽀개기',
date: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: 'React 잘해보자',
date: new Date().getTime(),
},
];
function App() {
const [todos, setTodos] = useState([mockData]);
return (
// 최상위 태그
<div className="App">
<Header />
<Editor />
<List />
</div>
);
}
export default App;
todo데이터는 임시데이터로 mockData로 저장을 했다.
todo데이터는 객체로서
id값, checkbox를 나타내는 isDone, 내용을 입력하면 나타낼 수 있는 content, 날짜가 포함되어 있어야한다.
8-1. ToDo Item 기능 구현
const [todos, setTodos] = useState([mockData]);
할 일을 입력하고 추가 버튼을 누르면 todos값을 변경시켜야한다.
그러기 위해서는 setTodos값을 설정하고 바꿔줘야한다.
이렇게 todos 배열에 값을 추가하기 위해 새로운 객체를 생성해준다.
const onCreate = (content) => {
const newTodo = {
id: 0,
isDone: false,
content: content,
date: new Date().getTime(),
};
};
⬇️ (주석 참고)
import './App.css';
import { useState } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
// 임시 데이터 생성
const mockData = [
{
id: 0,
isDone: false,
content: 'React 공부하기',
date: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: 'React 뽀개기',
date: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: 'React 잘해보자',
date: new Date().getTime(),
},
];
function App() {
const [todos, setTodos] = useState([mockData]);
const onCreate = (content) => {
const newTodo = {
id: 0,
isDone: false,
content: content,
date: new Date().getTime(),
};
// ⬇️ 잘못된 방법: todos(state 값은)는 상태변화를 호출 (setTodos)해서 값 변경가능하기 때문이다.
// todos.push(newTodo);
// setTodos를 호출해 인수로 값을 넣어야한다.
setTodos([newTodo, ...todos]);
};
return (
// 최상위 태그
<div className="App">
<Header />
<Editor />
<List />
</div>
);
}
export default App;
App 컴포넌트에 만든 onCreate함수를 호출하면 인수로 Editor 컴포넌트의 입력값을 전달해하면된다.
App 컴포넌트에서 Editor 컴포넌트에게 onCreate 함수를 props로 전달한다.
import './App.css';
import { useState } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
...
function App() {
...
};
return (
// 최상위 태그
<div className="App">
<Header />
<Editor onCreate={onCreate} />
<List />
</div>
);
}
export default App;
editor 컴포넌트에서 props( [URECA] Day 46 React 컴포넌트 — 오늘도 한줄 )로 구조 분해 할당( [JavaScript] 구조 분해 할당 — 오늘도 한줄 )으로
onCreate props를 받고 함수를 추가 버튼이 클릭되었을때 호출하면 된다.
import React from 'react';
import './Editor.css';
const Editor = ({ onCreate }) => {
return (
<div className="Editor">
<input placeholder="할 일을 추가해보세요."></input>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
onSubmit 함수 역시 만들어 준다.
onSubmit 함수를 만들대는 onCreate함수를 호출해서 인수로 input태그에 작성된 값을 전달하면 된다.
import React from 'react';
import './Editor.css';
const Editor = ({ onCreate }) => {
const onSubmit = () => {
onCreate();
};
return (
<div className="Editor">
<input placeholder="할 일을 추가해보세요."></input>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
input태그의 값을 state에 보관해준다.
import React, { useState } from 'react';
import './Editor.css';
const Editor = ({ onCreate }) => {
const [content, setContent] = useState('');
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
onCreate();
};
return (
<div className="Editor">
<input
value={content}
onChange={onChangeContent}
placeholder="할 일을 추가해보세요."
></input>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
e.target.value란?
그러면 할 일을 추가해보세요 값들이 코드 상에 content state에 보관된다.
추가 버튼이 클릭되면 onCreate 함수를 호출하며, 이때 인수로는 content state에 저장된 값이 전달된다.
import React, { useState } from 'react';
import './Editor.css';
const Editor = ({ onCreate }) => {
const [content, setContent] = useState('');
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
onCreate(content);
};
return (
<div className="Editor">
<input
value={content}
onChange={onChangeContent}
placeholder="할 일을 추가해보세요."
></input>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
즉, Editor 컴포넌트에서 추가 버튼이 눌리면, App 컴포넌트로부터 전달받은 onCreate 함수가 호출되며,
이때 인수로는 input 태그에 입력된 값이 전달된다.
그 결과 App 컴포넌트의 onCreate 함수가 실행되어 새로운 투두 아이템이 생성되고,
이 아이템은 setTodos 함수를 통해 todos state에 추가된다.
이를 통해 실제로 데이터가 추가되는 동작이 이루어지게 된다.
🔁 전체 동작 흐름 요약
- 사용자가 input에 텍스트를 입력 → content state에 값 저장
- 사용자가 "추가" 버튼 클릭 → onSubmit 실행
- onSubmit에서 onCreate(content) 호출 → 입력값이 부모 컴포넌트로 전달됨
useRef를 작성해 고유의 id값을 생성한다.
import './App.css';
import { useState, useRef } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
// 임시 데이터 생성
const mockData = [
{
id: 0,
isDone: false,
content: 'React 공부하기',
date: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: 'React 뽀개기',
date: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: 'React 잘해보자',
date: new Date().getTime(),
},
];
function App() {
const [todos, setTodos] = useState([mockData]);
const idRef = useRef(3);
const onCreate = (content) => {
const newTodo = {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
};
...
}
export default App;
idRef의 current값을 증가시켜준다.
비어있는 입력 방지, 비어있는 상태에서 추가버튼 누르면 focus되는 기능
import React, { useState, useRef } from 'react';
import './Editor.css';
const Editor = ({ onCreate }) => {
const [content, setContent] = useState('');
const contentRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
// 빈 입력값은 강제 링턴
if (content == '') {
contentRef.current;
return;
}
onCreate(content);
};
return (
<div className="Editor">
<input
ref={contentRef}
value={content}
onChange={onChangeContent}
placeholder="할 일을 추가해보세요."
></input>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
새로운 투두를 추가할 때 인풋 폼의 텍스트를 비워주는 기능
import React, { useState, useRef } from 'react';
import './Editor.css';
const Editor = ({ onCreate }) => {
const [content, setContent] = useState('');
const contentRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
// 빈 입력값은 강제 링턴
if (content == '') {
contentRef.current;
return;
}
onCreate(content);
setContent('');
};
return (
<div className="Editor">
<input
ref={contentRef}
value={content}
onChange={onChangeContent}
placeholder="할 일을 추가해보세요."
></input>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
setContent를 호출하고 빈 문자열로 상태를 초기화해주면 된다.
이렇게하면 새로운 데이터를 추가한 후 content state의 값이
빈 문자열로 초기화됨으로 브라우저에서 직접 데이터를 추가해보면 input의 값이 계속 초기화되는걸 확인할 수 있다.
엔터키누르면 투두 추가되는 기능
import React, { useState, useRef } from 'react';
import './Editor.css';
const Editor = ({ onCreate }) => {
const [content, setContent] = useState('');
const contentRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
// 엔터키누르면 투두 추가되는 기능
const onKeydown = (e) => {
if (e.keyCode == 13) {
onSubmit();
}
};
const onSubmit = () => {
// 빈 입력값은 강제 링턴
if (content == '') {
contentRef.current;
return;
}
onCreate(content);
setContent('');
};
return (
<div className="Editor">
<input
ref={contentRef}
value={content}
onChange={onChangeContent}
onKeyDown={onKeydown}
// onKeyDown은 사용자가 어떤 키를 눌렀는지 이벤트 객체의 keyCode 라는 프로퍼티에 저장된다.
placeholder="할 일을 추가해보세요."
></input>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
9. Read (투두리스트 렌더링)
import './App.css';
import { useState, useRef } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
...
function App() {
...
return (
...
{/* props로 전달 */}
<List todos={todos} />
</div>
);
}
export default App;
list 컴포넌트에서 구조분해 할당 문법으로 props로 넘겨준 todos state를 받아올 수 있다.
import React from 'react';
import ToDoItem from './ToDoItem';
import './List.css';
const List = ({ todos }) => {
return (
...
);
};
export default List;
배열에 담긴 값을 리스트 형태로 반복적으로 렌더링하려면 map메서드를 이용한다.
import React from 'react';
import ToDoItem from './ToDoItem';
import './List.css';
const List = ({ todos }) => {
return (
<div className="List">
<h4>TODO LIST</h4>
<input placeholder="검색어를 입력하세요." />
<div className="toDoWrapper">
{/* todo 메서드에 담긴 값을 props로 전달하기 위해서 중괄호 다음 스프레드연산자 작성 */}
{todos.map((todo) => {
return <ToDoItem {...todos} />;
})}
</div>
</div>
);
};
export default List;
그리고 todoItem으로 넘어간다.
import React from 'react';
import './ToDoItem.css';
const ToDoItem = ({ id, isDone, content, date }) => {
return (
<div className="ToDoItem">
<input checked={isDone} type="checkbox" />
<div className="content">{content}</div>
<div className="date">{date}</div>
<button>삭제</button>
</div>
);
};
export default ToDoItem;
import React from 'react';
import ToDoItem from './ToDoItem';
import './List.css';
const List = ({ todos }) => {
return (
<div className="List">
<h4>TODO LIST</h4>
<input placeholder="검색어를 입력하세요." />
<div className="toDoWrapper">
{/* todo 메서드에 담긴 값을 props로 전달하기 위해서 중괄호 다음 스프레드연산자 작성 */}
{todos.map((todo) => {
return <ToDoItem key={todo.id} {...todo} />;
})}
</div>
</div>
);
};
export default List;
- 검색기능
import React, { useState } from 'react';
import ToDoItem from './ToDoItem';
import './List.css';
const List = ({ todos }) => {
const [search, setSearch] = useState('');
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getFilteredData = () => {
if (search === '') {
// 검색어가 없을때 전체 todo를 return한다.
return todos;
}
// 1. todos배열의 filter메서드를 호출하고
// 그 결과값을 반환하고 있다.
// 어떤 기준으로 필터링하면? 배열의 모든 todoitem을 순회하면서
return todos.filter((todo) =>
// 대소문자 구분없이 모두 소문자로 검색되게
todo.content.toLowerCase().includes(search.toLocaleLowerCase())
);
// 위의 연산의 결과가 참인 투아만 필터링하는 것이다.
};
const filteredTodos = getFilteredData();
return (
<div className="List">
<h4>TODO LIST</h4>
<input
placeholder="검색어를 입력하세요."
value={search}
onChange={onChangeSearch}
/>
<div className="toDoWrapper">
{/* todo 메서드에 담긴 값을 props로 전달하기 위해서 중괄호 다음 스프레드연산자 작성 */}
{filteredTodos.map((todo) => {
return <ToDoItem key={todo.id} {...todo} />;
})}
</div>
</div>
);
};
export default List;
10. Update(투두 수정하기)
checkbox를 클릭할때 수정하는 기능을 만들어보겠다.
false → true
true → false 기능 필요
import './App.css';
import { useState, useRef } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
// 임시 데이터 생성
const mockData = [
{
id: 0,
isDone: false,
content: 'React 공부하기',
date: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: 'React 뽀개기',
date: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: 'React 잘해보자',
date: new Date().getTime(),
},
];
function App() {
const [todos, setTodos] = useState(mockData);
const idRef = useRef(3);
const onCreate = (content) => {
const newTodo = {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
};
// ⬇️ 잘못된 방법: todos(state 값은)는 상태변화를 호출 (setTodos)해서 값 변경가능하기 때문이다.
// todos.push(newTodo);
// setTodos를 호출해 인수로 값을 넣어야한다.
setTodos([newTodo, ...todos]);
};
const onUpdate = (targetId) => {
// todos State의 값들 중에
// targetId와 일치하는 id를 갖는 투아의 isDone변경
// 인수: todos 배열에서 targetId와 일치하는 id를 갖는 요소의 데이터만 딱 바꾼 새로운 배열
setTodos(
todos.map((todo) =>
todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo
)
);
};
return (
// 최상위 태그
<div className="App">
<Header />
<Editor onCreate={onCreate} />
{/* props로 전달 */}
<List todos={todos} />
</div>
);
}
export default App;
onUpdate함수를 어디서 호출해야할까?
import './App.css';
import { useState, useRef } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
...
function App() {
...
const onUpdate = (targetId) => {
// todos State의 값들 중에
// targetId와 일치하는 id를 갖는 투아의 isDone변경
// 인수: todos 배열에서 targetId와 일치하는 id를 갖는 요소의 데이터만 딱 바꾼 새로운 배열
setTodos(
todos.map((todo) =>
todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo
)
);
};
return (
// 최상위 태그
<div className="App">
...
{/* props로 전달 */}
<List todos={todos} onUpdate={onUpdate} />
</div>
);
}
export default App;
List 컴포넌트에서 전달받은 onUpdate함수를 구조분해할당으로 적어준다.
const List = ({ todos, onUpdate }) => {
return안에 props로 똑같이 넘겨준다.
import React, { useState } from 'react';
import ToDoItem from './ToDoItem';
import './List.css';
const List = ({ todos, onUpdate }) => {
...
const filteredTodos = getFilteredData();
return (
...
<div className="toDoWrapper">
{/* todo 메서드에 담긴 값을 props로 전달하기 위해서 중괄호 다음 스프레드연산자 작성 */}
{filteredTodos.map((todo) => {
return <ToDoItem key={todo.id} {...todo} onUpdate={onUpdate} />;
})}
</div>
</div>
);
};
export default List;
투아에서 input값이 변경되었을때 호출하면 된다.
import React from 'react';
import './ToDoItem.css';
const ToDoItem = ({ id, isDone, content, date, onUpdate }) => {
const onChangeCheckBox = () => {
// 인수로 id전달
onUpdate(id);
};
return (
<div className="ToDoItem">
<input
// onClick이 아니라 onChange를 사용한 이유는 요소가 인풋요소이기때문
onChange={onChangeCheckBox}
readOnly
checked={isDone}
type="checkbox"
/>
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button>삭제</button>
</div>
);
};
export default ToDoItem;
11. Delete 투두 삭제하기
app.jsx에서 삭제하는 함수 생성
// delete button
const onDelete = (targetId) => {
// targetId와 일치하지 않는 id만 필터링하여 새로운 배열을 만든다
setTodos(todos.filter((todo) => todo.id !== targetId));
};
...
return (
// 최상위 태그
<div className="App">
<Header />
<Editor onCreate={onCreate} />
{/* props로 전달 */}
<List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
List 컴포넌트에서 이 버튼이 클릭되었을때 호출하면 된다.
import React, { useState } from 'react';
import ToDoItem from './ToDoItem';
import './List.css';
const List = ({ todos, onUpdate, onDelete }) => {
...
const filteredTodos = getFilteredData();
return (
...
<div className="toDoWrapper">
{/* todo 메서드에 담긴 값을 props로 전달하기 위해서 중괄호 다음 스프레드연산자 작성 */}
{filteredTodos.map((todo) => {
return (
<ToDoItem
key={todo.id}
{...todo}
onUpdate={onUpdate}
onDelete={onDelete}
/>
);
})}
</div>
</div>
);
};
export default List;
투아에도 props로 마지막으로 받아서 버튼이 클릭되었을때 호출하면서 인수로는 아이디 전달
import React from 'react';
import './ToDoItem.css';
const ToDoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
const onChangeCheckBox = () => {
// 인수로 id전달
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="ToDoItem">
<input
// onClick이 아니라 onChange를 사용한 이유는 요소가 인풋요소이기때문
onChange={onChangeCheckBox}
readOnly
checked={isDone}
type="checkbox"
/>
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
export default ToDoItem;
🔁 전체 흐름 정리 요약
→ Editor에서 입력 (content state)
→ 버튼 클릭 or 엔터 → onSubmit 실행
→ App의 onCreate(content) 호출 → setTodos로 추가
[렌더링]
→ List로 todos 전달 → map으로 반복 렌더
→ ToDoItem에 각각 props 전달
[기능]
→ 체크박스 클릭: onUpdate → isDone 값 변경
→ 삭제 버튼 클릭: onDelete → 해당 항목 제거
→ 검색 입력 시: 필터링된 목록만 렌더링
useReducer로 바꿔보기
import './App.css';
import { useState, useRef, useReducer } from 'react';
import Header from './components/Header';
import Editor from './components/Editor';
import List from './components/List';
import Exam from './components/Exam';
// 임시 데이터 생성
const mockData = [
{
id: 0,
isDone: false,
content: 'React 공부하기',
date: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: 'React 뽀개기',
date: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: 'React 잘해보자',
date: new Date().getTime(),
},
];
function reducer(state, action) {
switch (action.type) {
case 'CREATE':
return [action.data, ...state];
case 'UPDATE':
return state.map((item) => (item.id === action.targetId ? { ...item, isDone: !item.isDone } : item));
case 'DELETE':
return state.filter((item) => item.id !== action.targetId);
default:
return state;
}
}
function App() {
const [todos, dispatch] = useReducer(reducer, mockData);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: 'CREATE',
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
};
// checkbox
const onUpdate = (targetId) => {
dispatch({ type: 'UPDATE', targetId });
};
// delete button
const onDelete = (targetId) => {
dispatch({ type: 'DELETE', targetId });
};
return (
// 최상위 태그
<div className="App">
{/* <Exam /> */}
<Header />
<Editor onCreate={onCreate} />
{/* props로 전달 */}
<List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
객체가 들어가는 복잡한 구조는 useReducer를 이용해 관리한다.
간단한 상태변화만 있다면 useState를 사용하면 좋다.
'🔷 React' 카테고리의 다른 글
[React] 최적화 🚀 (0) | 2025.04.24 |
---|---|
[React] useReducer (0) | 2025.04.23 |
[React] 라이프사이클 (0) | 2025.04.09 |
[React] 카운터앱, State Lifting (4) | 2025.04.09 |
[React] Hooks (0) | 2025.04.09 |