Search

리액트 To Do List 앱 만들기

React 시작하기 시리즈의 마지막 편입니다. 그동안 스터디했던 리액트 기본 지식을 토대로 프로젝트를 구성해보고 간단한 웹 애플리케이션을 만들어 보는 것으로 리액트 개발에 본격적으로 입문해 봅시다.
리액트 프로젝트를 구성하는 방법은 여러가지가 있지만, 가장 간단한 것은 Create React App을 사용하는 것입니다. Create React App은 리액트 프레임워크로 프로젝트를 시작할때 복잡한 환경 설정에 대해 신경쓰지 않고 코드 개발에만 집중할 수 있도록 초기 세팅을 도와줍니다.
시작하기에 앞서, 리액트 개발과 Create React App 사용을 위해서는 데스크톱에 Node.js가 설치되어 있어야 합니다. 설치되어 있지 않다면 Node.js 홈페이지에서 설치파일을 다운로드 받고 설치를 진행해주세요. LTS 버전이 안정적인 버전이므로 LTS 버전으로 설치하길 추천 드립니다.
Node.js가 준비되었다면, 터미널을 실행하고 리액트 앱 프로젝트를 생성해볼 디렉토리로 이동 후 아래 커맨드를 입력해서 리액트 프로젝트를 생성해 봅시다. 입력하면 create-react-app 모듈의 기본 탬플릿으로 리액트 프로젝트가 생성됩니다. 뒤에 붙은 my-first-app이 생성할 프로젝트의 이름입니다.
npx create-react-app my-first-app
Bash
복사
npx는 글로벌 모듈을 저장하지 않고 즉석에서 임시 파일만 받아 실행하는 명령어입니다. create-react-app 모듈을 글로벌 모듈로 설치하지 않은 상태에서 npx 명령어로 실행합니다.
터미널에 아래와 같은 안내가 출력되면 프로젝트가 성공적으로 생성된 것입니다.
Success! Created my-first-app at [설치 경로]/my-first-app Inside that directory, you can run several commands: npm start Starts the development server. npm run build Bundles the app into static files for production. npm test Starts the test runner. npm run eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing: cd my-first-app npm start
Bash
복사
안내가 추천해주는대로 명령어 두줄을 입력하고 생성한 리액트 앱을 실행해봅시다.
cd my-first-app npm start
Bash
복사
커맨드를 입력하면 자동으로 브라우저에서 http://localhost:3000 으로 연결되 아래 화면이 나오게 됩니다.
이제 리액트 앱을 만들기 위한 준비가 되었습니다.

 리액트 To Do List 앱 만들기

프론트엔드 개발 입문으로 가장 많이 만들어 보는 것중 하나가 To Do List일 겁니다. 리액트에서도 To Do List는 기능은 간단하지만 리액트의 기본적인 이해와 활용이 필요하므로 기본기를 다지기 위한 입문용으로 매우 좋은 케이스입니다.
To Do List를 만들어 보기에 앞서, 먼저 create-react-app으로 생성한 my-first-app 프로젝트의 파일 중 현재로썬 불필요한 파일들을 먼저 삭제하고 Pure한 상태에서 시작해보도록 합시다.
터미널에서 실행 중인 리액트 개발 모드를 종료하고, 아래 커맨드를 입력해 불필요한 파일들을 삭제합니다.
cd src rm App.test.js App.css logo.svg setupTests.js cd .. # 프로젝트 루트로 터미널 위치 귀환
Bash
복사
삭제 이후 프로젝트 구조가 아래와 같으면 됩니다.
├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src ├── App.js ├── index.css ├── index.js └── reportWebVitals.js
Plain Text
복사

To Do List 화면 디자인

우리가 만들어볼 To Do List 앱의 화면 시안부터 살펴봅시다. 할 일 입력 창과 추가하는 버튼이 있고, 추가한 할 일은 밑에 리스트로 나열됩니다. 클릭한 할 일은 체크 이모티콘과 함께 완료 상태가 되고 하단의 남은 일 카운트에서 차감됩니다. 리스트 중 이메일 보내기가 배경색이 회색인 것은 마우스 포인트를 올렸을때 나타나는 호버(hover) 이펙트를 나타낸 것입니다. 하단 우측의 완료 목록 삭제를 클릭하면 완료 상태의 할 일은 리스트에서 제거됩니다.
우선 화면 디자인 시안을 분석해서 리액트 컴포넌트 구성을 어떻게 하면 좋을지 한번 생각해 봅시다. 컴포넌트를 계획할 때 가장 중요한 것은 공통된 요소를 하나의 컴포넌트로 만들어 주는 것입니다. 할 일 리스트의 요소들은 반복적으로 재사용되므로 <ToDo /> 컴포넌트로 만들어 줄 겁니다. 사용자 상호작용이 발생하는 UI 요소도 현재는 한번씩만 사용되었지만, 일반적으로 여러 곳에서 재사용 되는 경우가 많으므로 각각 기능을 수행하는 컴포넌트로 구성해 줍시다.
아래 이미지처럼, 우리가 To Do List 앱을 완성하기 위해 만들어야 할 컴포넌트는 최상위 <App /> 컴포넌트 포함해서 총 5개입니다.
To Do List 앱 개발을 위한 CSS 스타일 코드는 미리 준비해 두었습니다. index.css 파일의 기존 코드를 모두 지우고, 아래 코드를 복사해서 붙여 넣어주세요.
./src/index.css
* { padding: 0; margin: 0; box-sizing: border-box; font: inherit; font-size: 18px; line-height: 1; color: #3C3C3B; } @font-face { font-family: 'NanumSquareRound'; src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_two@1.0/NanumSquareRound.woff') format('woff'); font-weight: normal; font-style: normal; } body { width: 100%; height: 100%; font-family: NanumSquareRound; background-color: #F5F5F5; } .app { display: flex; flex-direction: column; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 650px; height: 800px; overflow-y: auto; background: #FFFFFF; box-shadow: 0px 4px 10px #D9D9D9; border-radius: 5px; padding: 40px; } .app > div > .outline-input, .primary-button, .to-do { width: 100%; margin-bottom: 20px; } .app-title { font-weight: 800; font-size: 42px; margin: 16px auto 40px; text-align: center; } .app-form { flex-basis: 180px; } .app-list { flex-grow: 1; } .app-footer { display: flex; justify-content: space-between; } .outline-input { height: 50px; border: 1px solid #999999; border-radius: 5px; padding: 16px 15px; } .outline-input::placeholder { color: #D9D9D9; } .outline-input:focus { outline: none; } .primary-button { height: 50px; background: #45CDD0; border-radius: 10px; border: none; font-weight: 700; color: white; cursor: pointer; } .text-button { color: #999999; user-select: none; cursor: pointer; } .to-do { display: flex; align-items: center; height: 50px; background: #FFFFFF; border: 1px solid #D9D9D9; box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1); border-radius: 10px; padding: 15px; user-select: none; cursor: pointer; } .to-do:hover { background: #F5F5F5; } /* To Do 상태 완료 일때 */ .to-do[data-is-complete=true] > p:last-child { color: #BBBBBB; text-decoration: line-through; } .to-do > p:first-child { width: 18px; height: 18px; margin-right: 15px; }
CSS
복사

타이틀과 레이아웃 형성

우선 최상위 컴포넌트인 <App />에서 앱의 전체적인 레이아웃과 To Do List” 타이틀을 생성해 주겠습니다. App.js 파일의 기존 코드를 모두 삭제하고, 아래 코드를 전부 복사해서 붙여 넣어주세요.
./src/App.js
const App = () => { return ( <div className="app"> <h1 className="app-title">&#128466; To Do List</h1> <div className="app-form"> {/* <OutlineInput />과 <PrimaryButton />를 추가할 곳 */} </div> <div className="app-list"> {/* <ToDo />를 리스트 만큼 생성할 곳 */} </div> <div className='app-footer'> {/* 남은 일 개수와 <TextButton /> 추가할 곳 */} </div> </div> ); } export default App;
JavaScript
복사
h1 태그의 내용 중 &#128466; 로 작성되어 있는 것은 “” 이모티콘을 HTML에서 생성하는 코드입니다. 다른 이모티콘의 HTML 코드도 궁금하다면 유니코드 문자 백과사전에서 확인하세요.
현 상태에서 npm start 커맨드를 터미널에 입력해 앱을 다시 실행해 봅시다. http://localhost:3000에 아래 화면이 뜨게 됩니다.
이제 남은 컴포넌트들을 만들고 루트 컴포넌트인 <App />에 추가해보는 작업을 진행할 겁니다. 먼저 다른 컴포넌트들을 생성할 별도의 디렉토리를 만들어 줍시다. 경로는 ./src/components 입니다.
components 디렉토리가 생성 됬으면 아래에 우리가 추가할 4개의 컴포넌트 파일들을 미리 생성합시다. OutlineInput.js, PrimaryButton.js, TextButton.js, ToDo.js 파일들을 components 디렉토리 밑에 생성합니다.
... └── src ├── App.jsx ├── components │ ├── OutlineInput.js │ ├── PrimaryButton.js │ ├── TextButton.js │ └── ToDo.js ├── containers ├── index.css ├── index.js └── reportWebVitals.js
Plain Text
복사

OutlineInput 컴포넌트

추가할 To Do를 입력하는 <OutlineInput /> 컴포넌트를 만들겁니다. 아래 코드를 OutlineInput.js에 작성하세요. 공통으로 사용하는 입출력 컴포넌트를 만들때는 최대한 순수한 형태로 만드는 것이 좋습니다. 다른 컴포넌트나 특정 함수에 의존하지 않고 넘겨 받는 props로만 온전하게 기능하도록 만듭니다.
./src/comonents/OutlineInput.js
const OutlineInput = ({ value, placeholder, onChange }) => { return ( <input type="text" className="outline-input" placeholder={placeholder} value={value} onChange={onChange} // input의 입력 값이 변경되면 props로 받은 onChange 함수 실행 /> ) } export default OutlineInput;
JavaScript
복사

PrimaryButton, TextButton 컴포넌트

사용자 상호작용이 발생하는 버튼 컴포넌트도 마찬가지로 props로만 완전하도록 순수한 형태의 컴포넌트로 만드는 것이 좋습니다. 입력한 To Do를 리스트에 추가하는 버튼으로 사용될 <PrimaryButton />과, 완료된 To Do 목록을 삭제하는 버튼으로 사용될 <TextButton />를 아래 코드를 작성해서 만들어 줍시다.
./src/comonents/PrimaryButton.js
const PrimaryButton = ({ label, onClick }) => { return ( <button className="primary-button" onClick={onClick} // 클릭하면 props로 받은 onClick 함수 실행 > {label} </button> ) } export default PrimaryButton;
JavaScript
복사
./src/comonents/TextButton.js
const TextButton = ({ label, onClick }) => { return ( <p className="text-button" onClick={onClick} // 클릭하면 props로 받은 onClick 함수 실행 > {label} </p> ) } export default TextButton;
JavaScript
복사

ToDo 컴포넌트

추가된 할 일을 리스트에서 보여줄 <ToDo /> 컴포넌트를 만들어 보겠습니다. <ToDo /> 컴포넌트의 경우 완료하지 않은 To Do일 경우와 완료한 To Do인 경우 보여지는 것이 달라야 합니다. 이를 위해 To Do의 완료여부를 나타내는 isComplete 플래그를 props 로 받아서 사용할 겁니다.
isComplete = false
isComplete = true
HTML에서 data 속성을 사용하면 요소 내에 데이터를 저장할 수 있습니다. data 속성으로 저장한 값은 CSS에서도 접근이 가능합니다. 이를 통해 isComplete 여부에 따라 보여지는 텍스트 스타일을 다르게 처리하도록 할 수 있습니다. data 속성을 사용하는 방법은 간단합니다. 속성을 data-로 시작하도록 이름 지으면 됩니다. <ToDo />에서는 최상위 div 태그의 data-is-complete 속성에 isComplete 값을 저장하겠습니다.
완료 여부를 나타내는 “” 이모티콘은 AND(&&) 연산자를 사용해 isComplete 값이 true일때만 보여지도록 하겠습니다. 또한 컴포넌트를 클릭했을때 처리할 onClick 함수와 어떤 할 일인지 보여줄 value 텍스트도 props로 받고 적절한 위치에 대입해 줍니다.
./src/comonents/ToDo.js
const ToDo = ({ isComplete, // boolean 타입, 완료한 To Do인지 아닌지 여부 value, onClick }) => { return ( <div className="to-do" data-is-complete={isComplete} // HTML의 data속성에 isComplete 값 저장 onClick={onClick} > {/* isComplete이 true일때만 ✔️ 이모티콘 출력 */} <p>{isComplete && <span>&#10004;</span>}</p> <p>{value}</p> </div> ) } export default ToDo;
JavaScript
복사
미리 만들어 두었던 CSS 파일의 관련 코드의 data-is-complete 값을 사용하는 부분을 확인해봅시다. 값이 true 일때만 텍스트에 변경된 스타일을 적용합니다.
./src/index.css
... /* To Do 상태 완료 일때 */ .to-do[data-is-complete=true] > p:last-child { color: #BBBBBB; text-decoration: line-through; } ...
CSS
복사
.to-do 클래스가 적용된 요소의 data-is-complete 속성의 값이 true일때 마지막 p 태그에만 적용되는 스타일입니다.

App 루트 컴포넌트에 생성한 컴포넌트 적용

이제 마지막 단계입니다. 루트 컴포넌트인 <App />에서 그동안 만든 컴포넌트들을 가져와 추가하는 작업이 남았습니다. 먼저 App.js 파일 상단에 생성한 4개의 컴포넌트를 모두 임포트하고, state 사용을 위해 useState 함수도 임포트하는 코드를 추가합니다.
./src/App.js
import { useState } from 'react'; import OutlineInput from './components/OutlineInput'; import PrimaryButton from './components/PrimaryButton'; import TextButton from './components/TextButton'; import ToDo from './components/ToDo'; const App = () => { ...
JavaScript
복사
<OutlineInput />에서 입력된 값을 저장할 inputValue 상태와 추가된 모든 To Do 데이터들을 보관할 toDoList 상태를 만들어 줍니다.
./src/App.js
... const App = () => { const [inputValue, setInputValue] = useState(''); const [toDoList, setToDoList] = useState([]); ...
JavaScript
복사
이제 생성된 두가지의 state를 활용해서 To Do List 앱 기능에 사용되는 함수들을 구현해 봅시다.
1.
handleChange() : 입력한 새로운 할 일을 inputValue의 상태로 업데이트 하는 함수입니다. 이벤트 객체(event)를 인자로 받아서 변경된 값(event.target.value)을 inputValue 상태로 업데이트합니다.
2.
addToDo() : “할 일 추가하기” 버튼을 클릭하면 inputValue에 있는 입력된 할 일 문자열을 isComplete 플래그와 함께 객체로 만들어 toDoList 상태에 추가하는 함수입니다. setToDoList() 함수를 사용하기 때문에 push()로 추가하는 것이 아닌 현재 toDoList로 새로운 배열을 만들고 끝에 추가할 객체를 포함해서 업데이트하는 점을 유의하세요. 이렇게 하는 이유는 toDoList는 상태화 된 state이기 때문에 값을 직접적으로 수정해서는 안되기 때문입니다.
3.
toggleComplete() : <ToDo /> 컴포넌트를 클릭했을 때 해당 할 일의 isComplete 불리언 값을 토글하는 함수입니다. 현재 toDoListmap() 함수를 활용해 새로운 배열로 만들고, 해당하는 할 일의 isComplete를 토글 처리하여 업데이트 합니다.
4.
getUncompletedToDoList() : toDoList 배열에서 isComplete 값이 false인 것만 배열에 담아 반환하는 함수입니다. 완료되지 않은 할 일만 찾아줍니다.
5.
removeAllCompletedToDo() : “완료 목록 삭제” 버튼을 클릭했을때 실행할 함수입니다. 현재 toDoList에서 filter() 함수를 사용해 완료되지 않은 할 일만 새로운 toDoList로 업데이트하여 완료한 목록을 삭제합니다.
./src/App.js
... const [inputValue, setInputValue] = useState(''); const [toDoList, setToDoList] = useState([]); const handleChange = (event) => { // inputValue 상태를 이벤트의 요소 value 값으로 업데이트 setInputValue(event.target.value); } const addToDo = () => { // toDoList의 현재 배열을 전개 구문을 사용해 새로운 배열로 만들고 끝에 신규 할 일을 추가해 상태 업데이트 setToDoList((current) => [...current, { isComplete: false, value: inputValue }]); // toDoList 업데이트 이후 inputValue 빈 값으로 초기화 setInputValue(''); }; const toggleComplete = (index) => { // 현재 toDoList를 map 함수를 사용해 토글할 toDo의 isComplete 값을 역전 시킴 setToDoList((current) => current.map((toDo, toDoIndex) => { // 토글할 할 일의 index가 map 함수 순환 index와 같을때 if (toDoIndex === index) { // toDo 객체를 깊은 복사하기 위해 newToDo를 Object.assign 함수로 생성 const newToDo = Object.assign({}, toDo); // newToDo의 isComplete 값을 역전 newToDo.isComplete = !newToDo.isComplete; return newToDo; } else { // 토글할 할 일의 index가 map 함수 순환 index와 다를때는 기존 toDo를 그대로 사용 return toDo; } })); }; // 완료되지 않은 toDo만 찾는 filter 함수에 쓰일 조건식 함수 const isUncompletedToDo = toDo => !toDo.isComplete; // 완료되지 않은 toDo만 반환하는 함수 const getUncompletedToDoList = () => toDoList.filter(isUncompletedToDo); // 완료되지 않은 toDo만 toDoList로 업데이트하여 완료한 toDo는 제거 const removeAllCompletedToDo = () => { setToDoList((current) => current.filter(isUncompletedToDo)); }; return ( <div className="app"> ...
JavaScript
복사
리액트에서 배열 타입의 state를 업데이트 할때 직접적인 수정이 불가능해 push()와 같은 기존 배열을 변경하는 함수는 사용하지 못하므로 전개 구문(…Array) 문법을 활용해 새로운 배열을 만들고 아이템을 추가하거나 수정하는 형태를 많이 사용합니다. 배열의 내용을 변경하거나 특정 내용만 남겨야할 경우 map()filter() 함수가 특정한 작업을 수행한 뒤 온전한 새로운 배열을 반환하므로 이 두 함수를 활용하면 됩니다.
나머지 컴포넌트 코드까지 작성한 최종적인 App.js 파일의 코드는 아래와 같습니다.
./src/App.js
import { useState } from 'react'; import OutlineInput from './components/OutlineInput'; import PrimaryButton from './components/PrimaryButton'; import TextButton from './components/TextButton'; import ToDo from './components/ToDo'; const App = () => { const [inputValue, setInputValue] = useState(''); const [toDoList, setToDoList] = useState([]); const handleChange = (event) => { setInputValue(event.target.value); }; const addToDo = () => { setToDoList((current) => [...current, { isComplete: false, value: inputValue }]); setInputValue(''); }; const toggleComplete = (index) => { setToDoList((current) => current.map((toDo, toDoIndex) => { if (toDoIndex === index) { const newToDo = Object.assign({}, toDo); newToDo.isComplete = !newToDo.isComplete; return newToDo; } else { return toDo; } })); }; const isUncompletedToDo = toDo => !toDo.isComplete; const getUncompletedToDoList = () => toDoList.filter(isUncompletedToDo); const removeAllCompletedToDo = () => { setToDoList((current) => current.filter(isUncompletedToDo)); }; return ( <div className="app"> <h1 className="app-title">&#128466; To Do List</h1> <div className="app-form"> <OutlineInput placeholder="무엇을 해야하나요?" value={inputValue} onChange={handleChange} /> <PrimaryButton label="할 일 추가" onClick={addToDo} /> </div> <div className="app-list"> {toDoList.map((toDo, index) => <ToDo key={index} isComplete={toDo.isComplete} value={toDo.value} onClick={() => toggleComplete(index)} /> )} </div> <div className="app-footer"> <p>남은 일 :{getUncompletedToDoList().length}</p> <TextButton label="완료 목록 삭제" onClick={removeAllCompletedToDo} /> </div> </div> ); } export default App;
JavaScript
복사
그럼 이제, 완성한 To Do List 앱을 브라우저에서 확인해 볼까요?
완성된 To Do List 앱의 전체 코드는 아래 GitHub 링크에서 확인할 수 있습니다. https://github.com/crispy43/jinlog-react-todolist

 상태(state) 관리

React 시작하기 목록