React
[React]React를 입문해보자! (3)
React 입문 마지막 글로, TodoList를 만들며 CRUD 기능을 구현합니다.
1. TodoList를 제작해보자!
앱처럼 보이도록 TodoList를 감싸는 영역을 모바일 화면 형태로 디자인했습니다. 꼭 따라 하지 않아도 됩니다.

- gh-pages : https://kimdongeun12.github.io/react-todo/
1-1. 화면 구성하기

구상한 화면을 JSX로 작성하면 다음과 같습니다.
<TodoWrap> {/* 앱 컨테이너 */}
<Title /> {/* ToDoList */}
<TodoListsWrap> {/* 할 일 목록 컨테이너 */}
<TodoListCount /> {/* 체크가 안된 갯수 판별 */}
<ListsWrap>
<TodoListItem /> {/* 아이템이 있을경우 */}
<TodoListNone /> {/* 아이템이 없을경우 */}
</ListsWrap>
<TodoButton /> {/* 입력창 (추가 버튼) */}
</TodoListsWrap>
<TodoInput /> {/* 입력창 (추가 , 수정) */}
</TodoWrap>
2. 데이터 추가하기
2-1. 항목 데이터
{
id: 1,
item: '아이템',
checked: false,
}
항목은 id, text, checked 세 가지 key를 가진 객체로 구성했습니다.
벨로퍼트의 블로그를 참고해 Context API로 TodoContext.js를 만들고, 여기서 할 일 목록을 관리했습니다. 컴포넌트가 나뉘어 있어서 부모에서 자식으로 불필요한 데이터를 props로 계속 전달하고 싶지 않았기 때문입니다.
TodoContext.js
import React , {createContext , useReducer , useContext } from 'react'
const TodoStateContext = createContext(null);
// TodoLists state를 관리하는 TodoStateContext
const TodoControllContext = createContext(null);
// TodoLists를 컨토를하는 TodoControllContext
const TodosLists = [
// {id:1 , item: "todoList" , checked: false},
];
function todoReducer(TodoState, action) {
switch (action.type) {
case 'CREATE':
return TodoState.concat(action.todo);
case 'MODIFY':
return TodoState.map(todo =>
todo.id === action.todo.id ? { ...todo, item: action.todo.item } : todo
);
case 'CHECK':
return TodoState.map(todo =>
todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
);
case 'REMOVE':
return TodoState.filter(todo => todo.id !== action.id);
default:
return TodoState;
}
}
export function TodoContext({ children }) {
const [TodoState, dispatch] = useReducer(todoReducer, TodosLists);
return (
<>
<TodoStateContext.Provider value={TodoState}>
<TodoControllContext.Provider value={dispatch}>
{ children }
</TodoControllContext.Provider>
</TodoStateContext.Provider>
</>
);
}
export function useTodoState() {
return useContext(TodoStateContext);
}
export function useTodoController() {
return useContext(TodoControllContext);
}
useReducer로 state를 전역에서 관리할 수 있게 하고, 각 액션은 todoReducer 안에 정의했습니다.
App.js
function App() {
return (
<>
<AppWrap>
<Header />
<Todo/> {/* todo */}
</AppWrap>
</>
);
}
모바일 화면처럼 디자인한 AppWrap 안에 header와 Todo 컴포넌트를 배치했습니다.
Todo.js
function Todo() {
return (
<TodoContext>
<TodoWrap>
<Title>ToDoList</Title>
<TodoListsWrap>
<TodoLists />
</TodoListsWrap>
<TodoInput />
</TodoWrap>
</TodoContext>
);
}
Todo 컴포넌트 상위에 TodoContext를 두어 전역으로 데이터를 관리할 수 있게 했습니다. 이제 데이터를 불러오기만 하면 됩니다.
TodoContext.js
const TodosLists = [
{id:1 , item: "todoList" , checked: false},
];
TodoList.js
function TodoList() {
const todos = useTodoState();
// 추가 , 삭제 , 변경 된 체크값을 재배열함
const unChecked = todos.filter((todo) => !todo.checked);
return (
<>
<TodoListCount>할 일이 {unChecked.length}개 남았습니다~</TodoListCount>
<ListsWrap>
{todos.length > 0 ? (
todos.map((todo) => (
<TodoListItem
key={todo.id}
id={todo.id}
item={todo.item}
checked={todo.checked}
/>
))
) : (
<TodoListNone>할 일을 추가 해주세요~</TodoListNone>
)}
</ListsWrap>
<TodoButton />
</>
);
}
TodoContext에 정의한 useTodoState()로 todos를 가져오고, 삼항 연산자로 todos에 값이 있으면 TodoListItem을, 없으면 TodoListNone을 보여 줍니다.
2-2. 항목 추가
입력창을 모달로 만들었기 때문에, 먼저 모달을 여는 기능부터 구현합니다.
Todo.js
function Todo() {
const [isModal, setModal] = useState({
bool: false,
type: null,
listID: null,
});
const openModal = (id) => {
setModal({
bool: true,
type: "CREATE",
listID: null,
});
};
const closeModal = () => {
// 모달을 닫았을 경우 값 초기화
setModal({
bool: false,
type: null,
listID: null,
});
};
return (
<TodoContext>
<TodoWrap>
<Title>ToDoList</Title>
<TodoListsWrap>
<TodoLists openModal={openModal} />
</TodoListsWrap>
<TodoInput isModal={isModal} close={closeModal} />
</TodoWrap>
</TodoContext>
);
}
useState로 isModal의 값을 bool(모달이 열렸는지), type(추가인지 수정인지), listID(수정할 항목의 id)로 관리하고,
메서드는 props로 전달했습니다. (이 부분도 전역으로 관리하려고 React Portal과 Context를 검토했지만, 파일이 커져서 다음으로 미뤘습니다.)
그다음 입력 버튼이 있는 TodoLists에 openModal을 정의하고
TodoLists.js
function TodoList({ openModal }) {
const todos = useTodoState();
// 추가 , 삭제 , 변경 된 체크값을 재배열함
const unChecked = todos.filter((todo) => !todo.checked);
return (
<>
<TodoListCount>할 일이 {unChecked.length}개 남았습니다~</TodoListCount>
<ListsWrap>
{todos.length > 0 ? (
todos.map((todo) => (
<TodoListItem
key={todo.id}
id={todo.id}
item={todo.item}
checked={todo.checked}
openModal={openModal}
/>
))
// listComponents
) : (
<TodoListNone>할 일을 추가 해주세요~</TodoListNone>
)}
</ListsWrap>
<TodoButton onClick={openModal} />
</>
);
}
추가 버튼인 TodoButton에서 openModal을 호출하고, 수정 버튼이 있는 TodoListItem에도 openModal을 전달했습니다. (props를 여러 단계로 내려주는 이 부분은 개선이 필요하다고 생각합니다.)
TodoInput.js
function TodoInput({ isModal, close }) {
return (
<>
{
isModal.bool && (
<ModalWrap>
<TextWrap>
<TextField onSubmit={onSubmit}>
<input
id="todoText"
type="text"
placeholder="할 일을 적어주세용!"
autoFocus // 팝업창이 열리면 자동으로 인풋에 포커스가 가도록 설정
/>
{/* 버튼을 컴포넌트화 하여 이벤트 전달 */}
<StyleButton btnType="submit" buttonText="저장" />
<StyleButton
btnType="button"
buttonText="취소"
clickEvent={close}
/>
</TextField>
</TextWrap>
</ModalWrap>
)
}
</>
);
}
isModal의 bool 값이 true일 때 ModalWrap을 보여 주고, StyleButton의 onClick에 닫기 이벤트를 연결했습니다.
여기까지 하면 입력창이 모달로 열립니다. 이제 추가 기능을 구현합니다.
TodoInput.js
function TodoInput({ isModal, close }) {
const [inputValue, setValue] = useState(""); // inputValue을 useState로 관리
const onChange = (e) => setValue(e.target.value); // input의 value가 변할때마다 inputValue에 state값 변경
const todos = useTodoState(); // TodoContext.js에 선언한 TodoLists를 가져옴
const dispatch = useTodoController(); // TodoContext.js에 선언한 todoReducer에 보내는 역할
const nextId = useRef(todos.length); // 생성할때마다 다음 번호는 todos의 갯수의 다음번호로 설정
const onSubmit = (e) => {
e.preventDefault(); // form의 이벤트를 막기위해서 추가
if (inputValue.length <= 0) { // input의 값이 없을경우 이벤트 막기
alert("할 일을 적어주세용~");
return false;
}
let TodoID = nextId.current;
dispatch({
type: "CREATE",
todo: {
id: TodoID,
item: inputValue,
checked: false,
},
});
setValue("");
close();
};
return (
<>
{
isModal.bool && (
<ModalWrap>
<TextWrap>
<TextField onSubmit={onSubmit}>
<input
id="todoText"
onChange={onChange}
value={inputValue}
type="text"
placeholder="할 일을 적어주세용!"
autoFocus
</TextField>
</TextWrap>
</ModalWrap>
)
}
</>
);
}
TodoContext.js
function todoReducer(TodoState, action) {
switch (action.type) {
case 'CREATE':
return TodoState.concat(action.todo);
default:
return TodoState;
}
}
TodoInput에서 dispatch로 데이터를 보내면, TodoContext에서 action의 type을 확인해 CREATE일 때 concat으로 배열에 추가합니다.
3. 데이터 삭제하기
TodoListItem.js
function TodoListItem({ id, item, checked, openModal }) {
const dispatch = useTodoController();
const onRemove = (e) => {
dispatch({
type: "REMOVE",
id,
});
};
return (
<>
<CheckBoxList>
<CheckBox>
<input
id={`List${id}`}
type="checkbox"
/>
<label htmlFor={`List${id}`}>{item}</label>
</CheckBox>
<StyleButton
btnType="button"
buttonText="삭제"
clickEvent={() => onRemove(id)}
/>
</CheckBoxList>
</>
);
}
삭제 버튼에 onRemove를 연결하고, dispatch로 해당 항목의 id를 전달했습니다.
TodoContext.js
function todoReducer(TodoState, action) {
switch (action.type) {
case 'CREATE':
return TodoState.concat(action.todo);
case 'REMOVE':
return TodoState.filter(todo => todo.id !== action.id);
default:
return TodoState;
}
}
todoReducer에서 추가와 마찬가지로 action.type을 확인하고, filter로 해당 항목을 제거합니다.
4. 데이터 수정하기
체크 여부와 텍스트를 바꿀 수 있어야 해서, map으로 해당 항목만 찾아 수정하는 방식을 사용했습니다.
4-1. 체크 여부
TodoListItem.js
<CheckBox>
<input
id={`List${id}`}
type="checkbox"
checked={checked}
onChange={onChecking}
/>
<label htmlFor={`List${id}`}>{item}</label>
</CheckBox>
TodoContext.js
function todoReducer(TodoState, action) {
switch (action.type) {
case 'CREATE':
return TodoState.concat(action.todo);
case 'CHECK':
return TodoState.map(todo =>
todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
);
case 'REMOVE':
return TodoState.filter(todo => todo.id !== action.id);
default:
return TodoState;
}
}
TodoContext에 CHECK 케이스를 추가하고, map에서 checked: !todo.checked로 값을 뒤집었습니다. 텍스트 수정도 같은 방식으로 구현합니다.
4-2. 수정 버튼 만들기
앞에서 만든 모달에 추가와 함께 수정 케이스를 만듭니다. Todo.js에서 모달을 열 때 type을 구분하고, 그에 맞게 dispatch를 호출합니다.
Todo.js
function Todo() {
const [isModal, setModal] = useState({
bool: false,
type: null,
listID: null,
});
const openModal = (id) => {
// 모달을 열때 id값이 있을경우에 id값을 인자값으로 넘기고 isNaN으로 숫자인지 판별후
// id가 숫자일경우 MODIFY로 넘기고 없을경우에는 CREATE로 작성
if (!isNaN(id)) {
setModal({
bool: true,
type: "MODIFY",
listID: id,
});
} else {
setModal({
bool: true,
type: "CREATE",
listID: null,
});
}
};
TodoInput.js
function TodoInput({ isModal, close }) {
const [inputValue, setValue] = useState("");
const onChange = (e) => setValue(e.target.value);
const todos = useTodoState();
const dispatch = useTodoController();
const nextId = useRef(todos.length);
useEffect(() => {
//useEffect로 isModal의 값이 변했을때 MODIFY일 경우 인풋에 item을 적어주는 역할
if (isModal.type === "MODIFY") {
setValue(todos.map((todo) => {
return todo.id === isModal.listID ? todo.item : "";
}));
}
//useEffect로 isModal의 값이 변했을때 false일 경우 인풋에 item을 초기화 하는 역할
if (isModal.bool === false) {
setValue("");
}
}, [isModal , todos]);
const onSubmit = (e) => {
e.preventDefault();
if (inputValue.length <= 0) {
alert("할 일을 적어주세용~");
return false;
}
let TodoID = nextId.current;
if (isModal.type === "CREATE") {
TodoID = nextId.current++; //nextId 1씩 더하기
} else if (isModal.type === "MODIFY") {
//MODIFY일경우 기존의 아이디값을 넣어주기
TodoID = isModal.listID;
}
dispatch({
type: isModal.type,
todo: {
id: TodoID,
item: inputValue,
checked: false,
},
});
setValue("");
close();
};
}
TodoContext.js
function todoReducer(TodoState, action) {
switch (action.type) {
case 'CREATE':
return TodoState.concat(action.todo);
case 'MODIFY':
return TodoState.map(todo =>
todo.id === action.todo.id ? { ...todo, item: action.todo.item } : todo
);
case 'CHECK':
return TodoState.map(todo =>
todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
);
case 'REMOVE':
return TodoState.filter(todo => todo.id !== action.id);
default:
return TodoState;
}
}
이렇게 예외 처리를 하면서 추가와 수정을 함께 작성했습니다. CRUD 기능을 갖춘 TodoList를 만들어 보았는데, 만드는 것보다 블로그에 정리하는 데 시간이 더 오래 걸렸습니다.
자료를 공유해 주신 분들께 감사드립니다.
댓글 0