저번 포스팅에서는 메인 페이지를 구성하고 메인 페이지에 todo가 없을 때 표시할 컴포넌트를 만들고 메인 페이지에 적용을 했다. 저번 포스팅에 이어서 오늘은 먼저 writepage에 들어갈 WriteTodoForm컴포넌트를 만들고 textfield를 적용 시키는 것부터 시작 해보겠다.
먼저 components폴더에 WriteTodoForm.js파일을 생성해준다.
해당 파일의 코드를 아래와 같이 변경해준다.
mport { TextField } from "@mui/material";
export default function WriteTodoForm() {
return (
<>
<div className="flex flex-1 gap-5 flex-col p-16">
<TextField type="datetime-local" label="언제 해야 되나요?" focused />
<TextField
type="text"
label="무엇을 해야 하나요?"
multiline
className="flex flex-1"
slotProps={{
input: { className: "flex-1 flex-col" },
htmlInput: { className: "flex-1" },
}}
/>
</div>
</>
);
}
https://mui.com/material-ui/react-text-field/
React Text Field component - Material UI
Text Fields let users enter and edit text.
mui.com
내가 <TextField/>에 넘긴 props(mui에서는 api라고 함)는 아래 url로 들어가서 참고하면 된다.
https://mui.com/material-ui/api/text-field/
TextField API - Material UI
API reference docs for the React TextField component. Learn about the props, CSS, and other APIs of this exported module.
mui.com
코드를 다 작성했으면 WritePage로 이동후 기존에 적용했던 div태그 안에 write page라고 적힌 줄을 삭제하고 우리가 위에서 만든 WriteTodoFrom을 바로 적용시켜 보자.
import WriteTodoForm from "../components/WriteTodoForm";
function WritePage() {
return (
<>
<WriteTodoForm />
</>
);
}
export default WritePage;
https://github.com/TaeJinAn/happy_note/commit/4209506ddce7639a3f946f85838e7e7880d2da13
writetodoform 컴포넌트를 만들고 textfield 적용하기 · TaeJinAn/happy_note@4209506
taejin.ahn committed Sep 7, 2024
github.com
코드 작성이 끝나면 [control + shift + ~] 단축키를 사용하여 vscode terminal로 이동후에 terminal에 npm start 입력후 화면을 확인한다.

기본적인 form의 레이아웃은 완성이 되었고 다음은 해당 form의 button을 만들어 줄려고 한다. button에는 icon이 들어갈 예정인데 아이콘을 사용하기 위해서 아이콘 관련 library를 다운받아 줄 예정이다. icon 관련 library는 font awesome를 사용한다.
https://docs.fontawesome.com/web/use-with/react
Set Up with React
docs.fontawesome.com
공식홈페이지의 quick start를 참고하여 설치한다.
npm i --save @fortawesome/fontawesome-svg-core
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/free-regular-svg-icons
npm i --save @fortawesome/free-brands-svg-icons
npm i --save @fortawesome/react-fontawesome@latest
순서대로 모두 설치를 다 해주었다. 추가가 완료 되었으면 아래와 같이 WriteTodoForm의 코드를 변경해주자.
import { faMarker } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, TextField } from "@mui/material";
export default function WriteTodoForm() {
const onSubmit = (e) => {
e.preventDefault();
};
return (
<>
<form onSubmit={onSubmit} className="flex flex-1 gap-5 flex-col p-16">
<TextField type="datetime-local" label="언제 해야 되나요?" focused />
<TextField
type="text"
label="무엇을 해야 하나요?"
multiline
className="flex flex-1"
slotProps={{
input: { className: "flex-1 flex-col" },
htmlInput: { className: "flex-1" },
}}
/>
<Button variant="contained">
{/* <i class="fa-solid fa-marker"></i> */}
<FontAwesomeIcon icon={faMarker} className="mr-2"/>
<span>할일추가</span>
</Button>
</form>
</>
);
}
위의 코드에는 설명할 부분이 조금 있는데, 먼저 FontAwesomeIcon태그 font awesome의 최신 react문법이다. 보통은 html문법으로 i태그를 많이 사용하지만 FontAwesomeIcon을 사용해 보았다. 그리고 기존 html태그로 사용하고 싶으면 조건이 있는데 index.html 파일에서 cdn으로 font awesome을 불러와야 한다.
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
/>
해당 코드를 사용하면 위에서 주석처리한 i태그를 이용해서 아이콘을 사용할 수 있다. 두 방법다 어느것이 낫다 하기 힘드니 번갈아 가며 사용해보길.
그리고 기존의 div 태그가 form태그로 변경이 된 것을 볼 수 있는데 이는 데이터를 처리하기 위함이다. form태그 안에 있는 input 관련 데이터 입력 태그들은 사용자가 입력한 값을 전달해줄 수 있다. form의 onSubmit에는 onSubmit이라는 내가 선언한 함수를 직접 할당시켰다. 쉽게 이해를 돕기 위해 설명하자면 onSubmit={onSubmit} 은 onSubmit={(e) => {e.preventDefault();}와 같다. e.preventDefault() 는type이 submit인 버튼이 클릭됬을때 기본적으로 페이지 이동이 발생이 되는데 이 기본 액션을 막아준다.
버튼까지 수정이 끝났으면 WriteTodoForm은 다음 사진과 같은 모습이 된다.

https://github.com/TaeJinAn/happy_note/commit/0393dccdf69e1547b3405425472e7f11f0ec0364
icon 적용을 위한 fontawesome pakage 설치 및 icon 적용 · TaeJinAn/happy_note@0393dcc
taejin.ahn committed Sep 8, 2024
github.com
기본적인 외형이 모두 다듬어 졌으니 기능적인 디테일을 정리하자. onSubmit 함수에 validation alert처리를 추가하자. 데이터의 정합성을 검사하고 알림으로 화면에 띄워주는 기능을 추가하고 버튼의 type에 submit을 부여하여 버튼을 클릭시에 onSubmit 이벤트가 발생할수 있도록 form과 버튼을 연결해준다. 그리고 필요한 추가적인 tag의 속성들도 지정해준다.
WriteTodoForm을 다음과 같이 바꿔주자.
import { faMarker } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, TextField } from "@mui/material";
export default function WriteTodoForm() {
const onSubmit = (e) => {
e.preventDefault();
const form = e.target;
if (form.regDate.value.length == 0) {
alert("언제 해야 하는지 알려 주세요.");
}
if (form.content.value.length == 0) {
alert("무엇을 해야 하는지 알려 주세요.");
}
};
return (
<>
<form onSubmit={onSubmit} className="flex flex-1 gap-5 flex-col p-16">
<TextField
type="datetime-local"
label="언제 해야 되나요?"
focused
name="regDate"
/>
<TextField
name="content"
type="text"
label="무엇을 해야 하나요?"
multiline
className="flex flex-1"
slotProps={{
input: { className: "flex-1 flex-col" },
htmlInput: { className: "flex-1" },
}}
/>
<Button type="submit" variant="contained">
{/* <i class="fa-solid fa-marker"></i> */}
<FontAwesomeIcon icon={faMarker} className="mr-2" />
<span>할일추가</span>
</Button>
</form>
</>
);
}
변경이 끝나면 날짜와 내용을 입력하지 않을때에 화면 확인창을 볼 수 있다.


https://github.com/TaeJinAn/happy_note/commit/13bfb07972846a87c88d1fe91e25900f6a0abf68
form 적용 날짜와 콘텐츠 비어있시 alert처리 · TaeJinAn/happy_note@13bfb07
taejin.ahn committed Sep 8, 2024
github.com
여기까지 완료가 되면 본격적으로 WriteTodoForm의 기능을 구현하기 위한 개발을 진행해 보자. 먼저 states 폴더 안에 states.js를 만들어 준다. 혹은 state.js라고 명명해도 상관은 없다. states.js 파일을 생성한후에 동일한 폴더에서 atom.js와 index.js를 추가로 생성해준다. 생성이 완료되면 index.js를 다음과 같이 수정해준다.
export * from "./states.js";
export * from "./atoms.js";
같은 폴더 안에 있는 states.js 파일과 atom.js파일을 모두 다른 파일에서 import해 사용할수 있도록 하는 코드이다. 이후 atom.js파일로 들어가 states.js파일에서 사용할 atom을 설정한다.
import { atom } from "recoil";
export const todosAtom = atom({
key: "app/todosAtom",
default: {
todos: [], // 배열로 초기화
lastTodoId: 0
}
});
recoil library에서 atom을 가져와 이용하는데 atom을 사용하면 todosAtom을 전역변수처럼 사용할 수 있다. 리액트에는 "주입"이라는 개념이 존재하는데 부모 컴포넌트에서 자식 컴포넌트에게 필요한 데이터를 주입시켜 주어야하는데 recoil atom을 사용하면 주입이 없이 해당 컴포넌트에서 바로 원하는 데이터를 가져다 쓸 수 있다. 물론 atom으로 선언한 데이터 한정으로. 컴포넌트간에 데이터가 끊기는일 없이 사용할 수 있도록 전역변수를 선언 하는 방법이라고 알면 이해가 편할지도 모르겠다.
https://recoiljs.org/ko/docs/introduction/getting-started
Recoil 시작하기 | Recoil
React 애플리케이션 생성하기
recoiljs.org
해당 recoil시작하기 doc에 atom의 사용법이나 설명까지 자세히 나와 있다. 여러 컴포넌트에서 사용할 공통적인 데이터들은 미리 atom을 이용해서 선언해 놓으면 편하다.
다음은 states.js로 이동해서 useTodoState를 만들어 준다. 다음과 같이 수정한다.
import { useRef, useState } from "react";
import { useRecoilState } from "recoil";
import { todosAtom } from "./atoms";
export function useTodoState() {
const [todoData, setTodoData] = useRecoilState(todosAtom);
const lastTodoIdRef = useRef(todoData.lastTodoId);
lastTodoIdRef.current = todoData.lastTodoId;
const addTodos = (regDate, content) => {
console.log(regDate, content, "start addTodos");
const id = ++lastTodoIdRef.current;
const newTodo = {
regDate: regDate,
content: content,
id: id,
};
setTodoData({
...todoData,
lastTodoId: id,
todos: [newTodo, ...todoData.todos],
});
console.log("addTodos End todos");
return id;
};
return {
addTodos,
todoData,
};
}
console.log는 굳이 따라서 적지 않아도 된다. 하지만 개발 단계에서는 function의 시작점과 끝점을 표기해주는게 좋다. 경험상 console.log로 시작점과 끝점을 표기 해놨을 때 오류가 발생하면 더빨리 대처할 수 있었다. javascript의 오류는 콘솔에 표시되는 경우도 있지만 표시 안되는 경우가 훨씬 많다. try/catch처리를 미리 하거나 console.log로 찍어서 오류 발생지점을 찾는 시간을 줄이는 경우가 많다. 위코드에서 todoData를 useRecoilState를 이용하여 todosAtom으로 선언함으로써 recoil을 이용한 전역 변수 설정이 완료되었고 해당 todoData안에는 컴포넌트간에 공통적으로 사용할 데이터를 객체 형태로 저장할 예정이다. 랜더링 될때마다 아이디가 증가하는 경우를 막기 위해서 useRef를 사용하였다. lastTodoIdRef.current 와 todoData.lastTodoId를 의도적으로 동기화 시키지 않으면 데이터가 밀리거나 싱크가 맞지않는 경우가 있으니 주의할 것.
그리고 addTodos를 만들었다. 할일 폼에서 사용할 function으로 atom에 객체형태의 todo정보를 배열로 저장하고 추가하는 function이다. 그리고 실행이 끝나면 id를 리턴한다.
마지막으로 useTodoState의 리턴에 내가 사용할 변수들이나 function을 넣는다. return을 빼먹고 추가를 안하는 경우가 많으니 주의할 것.
useTodoState에서 할일 추가 function의 개발이 끝났으니 WriteTodoForm에 적용시켜보자. WriteTodoFrom을 다음과 같이 수정한다.
import { faMarker } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, TextField } from "@mui/material";
import { useTodoState } from "../states";
import { useEffect } from "react";
export default function WriteTodoForm() {
const todoState = useTodoState();
const onSubmit = (e) => {
e.preventDefault();
const form = e.target;
if (form.regDate.value.length == 0) {
alert("언제 해야 하는지 알려 주세요.");
return;
}
if (form.content.value.length == 0) {
alert("무엇을 해야 하는지 알려 주세요.");
return;
}
const newTodoId = todoState.addTodos(form.regDate.value, form.content.value);
console.log("WriteTodoFrom onSubmit End");
};
useEffect(() => {
console.log("todos updated!! : " + JSON.stringify(todoState.todoData.todos));
},[todoState.todoData.todos])
return (
<>
<form onSubmit={onSubmit} className="flex flex-1 gap-5 flex-col p-16">
<TextField
type="datetime-local"
label="언제 해야 되나요?"
focused
name="regDate"
/>
<TextField
name="content"
type="text"
label="무엇을 해야 하나요?"
multiline
className="flex flex-1"
slotProps={{
input: { className: "flex-1 flex-col" },
htmlInput: { className: "flex-1" },
}}
/>
<Button type="submit" variant="contained">
{/* <i class="fa-solid fa-marker"></i> */}
<FontAwesomeIcon icon={faMarker} className="mr-2" />
<span>할일추가</span>
</Button>
</form>
</>
);
}
useEffect부분은 빼도 상관없다. 데이터가 업데이트 될 때마다 문제없이 잘 업데이트 되었는지 체크하기 위에 디버깅용으로 찍어둔 것이다. states.js에서 만든 useTodoState를 사용하기 위해 선언하는 부분과 input의 내용이 비었을때 체크하는 분기문에서 return을 추가하였다. return을 추가해야지 validation의 기능을 제대로 수행하여 해당 분기 이후의 코드를 수행하지 않고 빠져나간다. addTodos는 id를 리턴하기 때문에 const newTodoId로 선언하여 리턴된 id값을 받아준다.
여기까지 완료되었으면 이제 추가까지는 정상적으로 처리가 되는 상태이다. 하지만 추가가 되어도 추가가 잘된건지 뭔지 현재 리스트를 구현하지 않았기 때문에 알 수가 없다. 그리고 통상적으로 어플에서는 무언가를 추가하거나 제거를 하면 알림이 뜨는데 알림조차 없다. 일단 알림을 추가하자. mui에는 snackbar라고 하는 알림 ui가 있다. 해당 ui를 이용해서 NoticeSnackBar컴포넌트를 만들 예정이다.
https://mui.com/material-ui/react-snackbar/
React Snackbar component - Material UI
Snackbars (also known as toasts) are used for brief notifications of processes that have been or will be performed.
mui.com
snackbar를 이용해서 컴포넌트를 만들기 전에 이번에는 state와 atom을 먼저 작성해보자.
atom.js를 다음과 같이 수정한다
import { atom } from "recoil";
export const todosAtom = atom({
key: "app/todosAtom",
default: {
todos: [], // 배열로 초기화
lastTodoId: 0
}
});
export const snackbarAtom = atom({
key: "app/snackbarAtom",
default: {
open: false,
severity: "success",
duration: 6000,
msg: ""
},
});
snackbar에서 사용할 값들의 기본값을 미리 지정해 두었다.
states.js를 다음과 같이 수정한다.
import { useRef, useState } from "react";
import { useRecoilState } from "recoil";
import { snackbarAtom, todosAtom } from "./atoms";
export function useTodoState() {
const [todoData, setTodoData] = useRecoilState(todosAtom);
const lastTodoIdRef = useRef(todoData.lastTodoId);
lastTodoIdRef.current = todoData.lastTodoId;
const addTodos = (regDate, content) => {
console.log(regDate, content, "start addTodos");
const id = ++lastTodoIdRef.current;
const newTodo = {
regDate: regDate,
content: content,
id: id,
};
setTodoData({
...todoData,
lastTodoId: id,
todos: [newTodo, ...todoData.todos],
});
console.log("addTodos End todos");
return id;
};
return {
addTodos,
todoData,
};
}
export function useSnackBarState() {
const [snackbarData, setSnackbarData] = useRecoilState(snackbarAtom);
const handleClose = () => {
setSnackbarData({ ...snackbarData, open: false });
};
const openSnackBar = (msg, severity = "success", duration = 6000) => {
console.log("openSnackBar start!!!");
setSnackbarData({
open: true,
severity: severity,
duration: duration,
msg: msg,
});
};
return {
snackbarData,
handleClose,
openSnackBar,
};
}
아래에 useSnackBarState()가 추가 되었고 해당 알림 기능또한 추가 뿐만이 아닌 삭제 및 수정에서도 사용할 수 있기 때문에 recoil을 사용하여 atom으로 데이터를 만들었다. handleClose는 snackbar가 닫힐때 동작할 function이고 openSnackBar은 여러 컴포넌트에서 snackbar를 열고 싶을때 사용할 function이다. useTodoState와 마찬가지로 사용할 function과 변수를 return에 넣어준다.
state 와 atom을 모두 만들었으니 마지막으로 컴포넌트를 만들어준다. components폴더에 NoticeSnackBar.js파일을 만들고 아래와 같이 작성해준다.
import { Alert, Snackbar } from "@mui/material";
import { useSnackBarState } from "../states";
export default function NoticeSnackBar() {
const snackBarState = useSnackBarState();
return (
<>
<Snackbar open={snackBarState.snackbarData.open} autoHideDuration={snackBarState.snackbarData.duration} onClose={snackBarState.handleClose}>
<Alert
onClose={snackBarState.handleClose}
severity={snackBarState.snackbarData.severity}
variant="filled"
sx={{ width: "100%" }}
>
{snackBarState.snackbarData.msg}
</Alert>
</Snackbar>
</>
);
}
위 사용법에 관련한 내용은 위에 첨부한 공식홈페이지 url로 들어가 Use with Alerts항목을 찾아 참고하면 된다.

NoticeSnackBar.js의 작성이 끝나면 WritePage에 가서 적용해준다. WrtiePage에 선언하는 이유는 전역 상태 관리 및 UI 요소 중복 방지를 위해, Snackbar와 같은 전역적인 UI 요소는 상위 컴포넌트에 선언하고, 하위 컴포넌트에서 해당 알림을 트리거하는 방식으로 사용한다. WritePage.js를 다음과 같이 수정한다.
import NoticeSnackBar from "../components/NoticeSnackBar";
import WriteTodoForm from "../components/WriteTodoForm";
function WritePage() {
return (
<>
<NoticeSnackBar />
<WriteTodoForm />
</>
);
}
export default WritePage;
이후 WriteTodoForm에서 할일 추가가 끝나면 NoticeSnackBar를 트리거하여 호출하도록 수정한다. 다음과 같이 useSnackBarState를 선언하는 부분과 해당 addTodos가 끝난뒤 snackBarState의 openSnackBar함수를 호출해주는 부분을 추가한다.
import { faMarker } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, TextField } from "@mui/material";
import { useSnackBarState, useTodoState } from "../states";
import { useEffect } from "react";
export default function WriteTodoForm() {
const todoState = useTodoState();
const snackBarState = useSnackBarState();
const onSubmit = (e) => {
e.preventDefault();
const form = e.target;
if (form.regDate.value.length == 0) {
alert("언제 해야 하는지 알려 주세요.");
return;
}
if (form.content.value.length == 0) {
alert("무엇을 해야 하는지 알려 주세요.");
return;
}
const newTodoId = todoState.addTodos(form.regDate.value, form.content.value);
snackBarState.openSnackBar(`${newTodoId}번 할일이 추가 되었습니다.`);
console.log("WriteTodoFrom onSubmit End");
};
useEffect(() => {
console.log("todos updated!! : " + JSON.stringify(todoState.todoData.todos));
},[todoState.todoData.todos])
return (
<>
<form onSubmit={onSubmit} className="flex flex-1 gap-5 flex-col p-16">
<TextField
type="datetime-local"
label="언제 해야 되나요?"
focused
name="regDate"
/>
<TextField
name="content"
type="text"
label="무엇을 해야 하나요?"
multiline
className="flex flex-1"
slotProps={{
input: { className: "flex-1 flex-col" },
htmlInput: { className: "flex-1" },
}}
/>
<Button type="submit" variant="contained">
{/* <i class="fa-solid fa-marker"></i> */}
<FontAwesomeIcon icon={faMarker} className="mr-2" />
<span>할일추가</span>
</Button>
</form>
</>
);
}
여기 까지 완료되면 할일추가 이후에 왼쪽 하단에 snackbar 알림까지 완료되었다. 실제 화면으로 테스트를 해보자.


나는 2번 실행했기 때문에 2번 할일이 추가 되었다고 알림이 떴다. 오늘은 state와 atom을 작성하고 recoil을 사용하여 전역변수 처리를 진행하고 WriteTodoForm과 WrtieTodoForm에 필요한 NoticeSnackBar까지 만들어 보고 기능을 적용시켜 보았다.
https://github.com/TaeJinAn/happy_note/commit/840311f41bc94e74e823416ec379c0430f126baf
snackbar알림 state추가 및 여러개의 아톰 단일 객체화 · TaeJinAn/happy_note@840311f
taejin.ahn committed Sep 8, 2024
github.com
크게 어려운 내용이 없었을거라 생각한다. 개발자가 되기 위해서 공부를 하는사람은 이것을 어렵다고 생각하면 안된다. 절대로. 실제 프로젝트에서는 배우지 않은 문제들이 눈앞에 닥치는 경우가 많다. 정보도 없는 오류, 성능 이슈등등 하지만 이런 어려움을 극복했을때 주어지는 보상은 달다. 금전적인 보상만을 얘기하는 것이 아니다. 보수는 경험으로도 받을 수 있는것이다. 어려움을 극복함으로써 비슷한 문제들은 해결할 수 있는 한계단 더 성장한 개발자가 되는 것이다. 열심히 공부하자.
'React' 카테고리의 다른 글
| Todo앱을 만들어 보자 -6- (22) | 2024.10.06 |
|---|---|
| Todo앱을 만들어 보자 -5- (6) | 2024.09.28 |
| Todo앱을 만들어 보자 -3- (2) | 2024.09.22 |
| React 프로젝트를 생성하고 Todo앱을 만들어 보자 -2- (22) | 2024.09.18 |
| React 프로젝트를 생성하고 Todo앱을 만들어 보자 -1- (12) | 2024.09.10 |