React in 30 minutes

참고로, 이 문서는 2022년 9월13일 기준으로 작성되었습니다. Frontend 기술은 하루가 다르게 변하기 때문에 당신이 이 문서를 보는 시점이 1년이 지난 시점이라면 여기에서 실행하는 코드중에 실행이 안되는 코드가 있을수 있다는점 알려드립니다.

React를 시작하려면 컴퓨터에 node가 설치가 되어있어야합니다. node가 설치가 되지 않았다면 여기를 누르시고 node를 설치한 뒤에 다시 와서 계속해주세요.

node가 정상적으로 설치가 되었다면 npx가 함께 설치가 되었을겁니다. 프로젝트를 진행할 폴더를 새로 하나 만드시고 그 안에 들어가셔서 npx create-react-app .명령을 실행해주세요. 저는 폴더명을 todolist라고 했습니다. TODO리스트를 관리하는 프로그램을 만들거거든요.

$ cd todolist
$ npx create-react-app .
...
Success! Created my_react_project at /Users/me/todolist
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 /Users/me/todolist
  npm start

한참을 설치한 뒤에 위와 같이 성공적으로 프로젝트가 생성되었다는 메세지가 뜨고, 이하 실행가능한 npm 명령어들을 보여줍니다.

  • npm start: 애플리케이션을 시작하는 명령어로써 웹서버를 실행합니다.
  • npm run build: 수정된 파일들을 실서버에 적용하는 명령어
  • npm test: 선행 정의된 각종 테스트를 실행하는 명령어
  • npm run eject: 프로젝트에 숨겨져있는 모든 설정을 밖으로 추출하는 명령인데 이를 통해 webpack과 babel의 자유로운 세팅이 가능하게 됩니다. 단, 이 명령은 한번 실행하면 되돌리기 힘들기때문에 반드시 정확하게 이해한 후에 실행하시기 바랍니다.

프로젝트를 생성하신 뒤 폴더를 보시면 옆의 구조로 파일들이 자동으로 생성되어있는 것을 확인 하실 수 있으세요. 클라이언트가 웹사이트에 접속하게 되면 가장 먼저 실행되는 파일이 바로 index.js입니다. index.js를 열어보시면 상수 root에 ReactDOM.createRoot(document.getElementById('root')) root라는 ID를 가진 HTML객체를 가져와서 ReactDOM으로 문서를 생성합니다. 그리고 그 객체 안에 내용을 넣도록 root.render 함수를 호출 하는데요. 그때 인자로 들어가는 태그의 이름이 아래코드에서 보시는 것과 같이 App이라고 명시되어 있습니다. App이라는 컴포넌트를 가져다가 화면에 출력해주겠다는 뜻입니다.

root.render(
    <App />
);

App이라는 컴포넌트는 바로 App.js에 정의된 App이라는 이름으로 공유된 함수의 결과입니다. App.js에 들어가보시면 App()함수에서 화면에 보여줄 코드를 반환하고 있고, 이 함수를 맨 아래 export default App명령을 통해서 외부에서 접속할수 있게 제공합니다.

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

위의 코드가 제대로 실행이 되는지 서버를 한번 띄워보도록 하겠습니다. 아래 명령을 실행할때에는 반드시 해당 프로젝트 폴더에 들어가신 후 실행 하셔야합니다.

$ npm start

위의 명령어를 실행하면 브라우저창이 자동으로 뜨면서 http://localhost:3000/를 로딩합니다.

위의 이미지가 나오면 서버가 성공적으로 실행이 된 것입니다.

현재 보이는 디자인은 React가 default로 보여주는건데 우리가 만들 프로젝트에서는 이게 필요없으니까 App함수에서 null을 반환하도록 변경해볼게요. 그리고 App.css와 react logo 파일도 import할 필요 없으니까 지울게요.

function App() {
  return null
}

export default App;

저장과 동시에 브라우저의 웹화면이 백지화 되는 것을 보실수 있으실겁니다.

Component

자 그럼 이제, 새로운 컴포넌트를 하나 만들어 볼까요? 일단 현재 TODO리스트를 화면에 보여주는 걸 한번 구현해 볼게요. 코드를 App에 바로 구현하는것 보다 TODO리스트를 모듈화 해서 다른 파일에 컴포넌트로 구현해서 여기서 불러다 쓰면 코드가 더 깔끔할것 같으니 일단 여기서는 TodoList라는 컴포넌트를 불러다가 출력해주는걸로 합니다. 이렇게 하면 index.js가 App을 호출하고 App은 TodoList를 호출하여 결과적으로 화면에 출력해주게 되는 거죠.

function App() {
  return (
    <TodoList />
  );
}

<TodoList />는 생긴게 마치 HTML 태그같기도 하지만 그건 아니고 react에서 제공하는 컴포넌트를 호출하는 형식입니다. 아직 TodoList라는 컴포넌트는 만들지 않았지만 앞으로 만들 TodoList를 여기에 넣겠다고 선언한거에요. 그럼 이제 TodoList컴포넌트를 만들어 볼게요. App.js와 같은 폴더에 TodoList.js라는 파일을 생성해주세요. 그리고 아래와 같이 함수를 하나 선언해서 export해주세요.

export default function TodoList() {
	return (
		<div>
		
		</div>
	)
}

그리고 다시 App.js로 돌아가서 우리가 방금 만든 컴포넌트를 import해주세요.

import TodoList from './TodoList'

function App() {
  return (
    <TodoList />
  );
}

export default App;

이렇게 하면 우리가 만든 TodoList를 App이 반환하는 결과 안에 넣게 됩니다. 결과가 잘 들어가는지 TodoList를 한번 수정해 볼까요? TodoList의 결과값에 Hello World를 넣어보면 브라우저 화면에 Hello World가 출력됩니다.

export default function TodoList() {
	return (
		<div>
                     Hello World
		</div>
	)
}

Pass the Variables between Components

Todo리스트를 보여주려면 Todo리스트를 저장할 배열이 있어야겠지요? App()안에 todos라는 배열을 하나 선언해줍니다. 그리고 그 안에 할 일 두개를 저장해볼게요. 그리고 그 배열, todos를 TodoList컴포넌트에 전달해줍니다. todos라고 같은 이름으로 전달하는게 덜 헷갈리겠죠.

function App() {
  const todos = ['Todo 1', 'Todo 2']

  return (
    <TodoList todos={todos}/>
  );
}

그럼 이젠 TodoList에서 배열을 전달 받아야겠죠? 함수의 인자로 todos를 아래와 같이 넣어서 받습니다. 그리고 전달 받은 배열을 화면에 하나씩 돌아가면서 출력해줄건데 이때 배열의 기본 함수인 map()을 이용해서 배열 속 아이템 하나씩 돌면서 화면에 출력을 해주도록 합니다. 브라우저에 Todo 1과 Todo 2가 보이시나요?

export default function TodoList({ todos }) {
	return (
		todos.map(todo => {
			return <div key={todo}>{todo}</div>
		})
	)
}

Todo Item

이제는 사용자한테서 Todo리스트를 하나씩 입력받아서 등록하도록 할건데요. 입력상자는 Todo리스트의 맨 끝에 넣도록 하겠습니다. TodoList컴포넌트를 삽입한 바로 다음에 입력상자와 추가버튼을 아래와 같이 넣도록 하겠습니다.

function App() {
  const todos = ['Todo 1', 'Todo 2']

  return (
    <>
      <TodoList todos={todos}/>
      <input type="text" />
      <button >Add Todo</button>
      <div>0 left to do</div>
    </>
  );
}

위의 코드에서 자세히 보시면요. 결과값 처음과 끝에 빈 태그를 넣어주었어요. 그 이유는 컴포넌트를 반환할때는 반드시 닫는 태그로 끝나야 하기 때문이죠. 아까 TodoList태그 하나만 있을때는 혼자 열고 혼자 닫기 때문에 상관이 없는데 태그가 여러개 추가되면서 닫히지 않은 태그로 인식이 되기때문에 처음과 끝에 의미없는 태그를 넣어서 마무리가 잘 된 상태의 코드를 넘겨주는 거죠. 사실 저 빈 태그는 <React.Fragment>의 약칭입니다.

useRef and handleAddTodo

사용자에게서 Todo아이템을 입력받기 위해서는 사용자가 텍스트상자에 입력한 내용을 읽어와야합니다. 이때 유용하게 사용할수 있는 react함수가 바로 useRef입니다. useRef는 react라이브러리에서 제공하는 기본 함수지만 사용하기 위해서는 반드시 명시를 해주셔야합니다. App.js의 맨 위에 아래와 같이 useRef를 import해주세요. 그리고 App()안에 todoNameRef이름의 상수를 하나 선언하고 해당 함수를 할당합니다. 그리고 아까 추가했던 입력상자의 ref attribute에 todoNameRef를 asstign합니다. 이렇게 하면 입력상자를 todoNameRef라는 이름으로 간편하게 불러서 사용할수 있습니다.

import { useRef } from 'react'
...
function App() {
  const todoNameRef = useRef()
...
   <input ref={todoNameRef} type="text" />

이번에는 버튼을 클릭하면 호출해서 입력받은 Todo아이템을 처리할 함수를 선언하도록 하겠습니다. App()안에 handleAddTodo라는 함수를 하나 선언하고 e를 인자로 받도록 합니다. 그리고 아까 정의한 todoNameRef의 값을 가져와서 name이라는 상수에 저장합니다. 그리고 버튼의 onClick 속성에 방금 정의한 handleAddTodo라는 함수를 아래와 같이 할당합니다.

function App() {
...
  function handleAddTodo(e) {
    const name = todoNameRef.current.value
  }
...
  <button onClick={handleAddTodo}>Add Todo</button>

useState

이제 사용자가 Todo아이템을 입력하면 todos라는 배열에 추가해서 화면에 보여주는 걸 해보겠습니다. 아까는 todos라는 배열을 임의로 선언했는데 사용자가 새로운 아이템을 입력할때마다 수동으로 배열에 값을 추가할수도 있지만 react에는 자주 변경되는 아이템을 깔끔하게 관리할수 있도록 useState라는 모듈을 제공하고 있습니다. 이 모듈은 로컬에 저장공간을 확보하고 해당 저장공간에 정보를 담는일을 손쉽게 할수 있도록 해줍니다. 사용방법은 일단 react라이브러리에서 useState를 import하고 기존에 임의로 선언했던 todos와 두개의 Todo아이템 대신에 useState함수를 호출하여 아래와 같이 값을 받아옵니다. useState에 인자로 들어가는 값은 저장공간의 초기값으로 우리는 빈배열을 넘겨주어 앞으로 이 공간에 배열이 들어가게 될것임을 알립니다. 그리고 useState이 반환하는 결과값은 배열 안에 두개의 값을 넣어서 반환하는데 0번방의 값은 현재 저장 공간에 저장되어 있는 변수의 포인터이고, 1번방에는 해당 저장공간에 저장을 할수 있게 해주는 함수의 포인터를 반환합니다. 우리는 이 함수를 setTodos라는 이름으로 받아서 사용하도록 하겠습니다. 그리고 handleAddTodo에서 입력받은 Todo아이템을 useState를 통해 제공받은 setTodos를 통해서 배열에 추가를 합니다. 아래 코드를 보시면 setTodos를 호출하면서 기존 배열, prevTodos에 새로운 아이템 name을 추가하고 있는 모습을 보실수 있습니다. ...문법에 대해 생소하신 분들은 자바스크립트에서 점 3개(…) 활용법을 참조해주세요. prevTodos는 setTodos를 호출하면 제공받게 되는 기존 저장공간의 포인터입니다. 인자로 함수를 넘겨주고 함수가 실행된 결과를 제공받는 컨셉이 생소하신 분들은 Understanding Array.map()을 한번 읽어보세요. 이런 형식의 함수들을 이해하는데 도움이 되실겁니다. 그리고 배열방에 추가를 한 뒤에 화면에 보이는 입력창은 null로 비워줄게요.

import { useState, useRef } from 'react'
...
function App() {
  const [todos, setTodos] = useState([])
...
  function handleAddTodo(e) {
    const name = todoNameRef.current.value
    if (name === '') return
    setTodos(prevTodos => {
      return [...prevTodos, name]
    })
    todoNameRef.current.value = null
  }

브라우저창을 열고 텍스트상자에 Todo아이템을 입력한뒤 “Add Todo”버튼을 눌보세요. 할일 목록에 하나씩 추가되는게 확인이 되시면 다음으로 넘어갑니다. 혹시 오류가 나는 분들을 위해서 지금까지의 코드를 정리해드릴게요.

App.js

import { useState, useRef, useEffect } from 'react'
import TodoList from './TodoList'

function App() {
  const [todos, setTodos] = useState([])
  const todoNameRef = useRef()

  function handleAddTodo(e) {
    const name = todoNameRef.current.value
    console.log(name)
    if (name === '') return
    setTodos(prevTodos => {
      return [...prevTodos, name]
    })
    todoNameRef.current.value = null
  }

  return (
    <>
      <TodoList todos={todos}/>
      <input ref={todoNameRef} type="text" />
      <button onClick={handleAddTodo}>Add Todo</button>
      <div>0 left to do</div>
    </>
  );
}

export default App;

TodoList.js

export default function TodoList({ todos }) {
	return (
		todos.map(todo => {
			return <div key={todo}>{todo}</div>
		})
	)
}

string to object

현재는 입력한 문자열만 배열에 저장했는데요. 데이타를 변경하거나 삭제하는데 원활한 관리를 위해서 각 문자열에 ID를 부여하도록 하겠습니다. 고유한 ID를 생성하기 위해서 uuid라는 패키지를 node에 설치하도록 할게요. 잠시 코드를 떠나 터미널을 열고 아래의 명령어를 실행합니다.

$ npm install uuid

다시 코드로 돌아와서 uuid패키지를 App.js에 추가해줍니다. uuid패키지에 있는 v4라는 함수인데 여기서는 uuidv4라고 부르도록 import했어요. 그리고 나서 handleAddTodo함수에 배열에 name대신 객체를 넣도록 할게요. 객체는 id, name 그리고 할일을 완료했을때 마킹해주는 complete이라는 변수도 하나 넣을게요. 초기값은 아직 할일이 완료가 안되었으니 false로 설정합니다.

...
import { v4 as uuidv4 } from 'uuid';
...
  function handleAddTodo(e) {
    const name = todoNameRef.current.value
    if (name === '') return
    setTodos(prevTodos => {
      return [...prevTodos, {id: uuidv4(), name: name, complete: false}]
    })
    todoNameRef.current.value = null
  }
...

데이타의 형식이 바뀌었으니 보여지는 쪽에서도 바꿔 주어야겠지요? TodoList.js를 열어서 아래와 같이 변경합니다.

export default function TodoList({ todos }) {
	return (
		todos.map(todo => {
			return <div key={todo.id}>{todo.name}</div>
		})
	)
}

Todo 컴포넌트

이제 각 할일들 앞에 체크박스를 넣어서 일이 완료가 되었는지 마킹을 하도록 할건데요. 코딩을 TodoList.js에서 할수도 있지만 저는 뭐든 그룹그룹 묶어주는게 좋아서요. 체크박스와 할일을 보여주는 한줄짜리 컴포넌트를 만들어서 각 배열방의 값으로 호출해서 보여주도록 하겠습니다. TodoList.js에서 TodoList함수에서 인자로 넘겨받은 todos배열을 하나씩 돌면서 Todo라는 컴포넌트를 호출해서 내용을 구성해보도록하겠습니다.

import Todo from './Todo'

export default function TodoList({ todos }) {
  return (
    todos.map(todo => {
      return <Todo key={todo.id} todo={todo} />
    })
  )
}

아직 Todo컴포넌트가 없으니 만들어 줘야겠죠? 같은 폴더안에 Todo.js를 생성하고 아래와 같이 코딩해주세요.

export default function Todo({ todo }) {
  return (
    <div>
      <label>
        <input type="checkbox" checked={todo.complete} />
        {todo.name}
      </label>
    </div>
  )
}

toggleTodo()

이제 나열된 할일을 완료했을때 체크박스를 클릭해서 업무를 완료 했다고 표시하는 코딩을 한번 해볼게요. 우선 App.js에 toggleTodo라는 이름으로 함수를 하나 선언할게요. 이 함수는 각 할일의 ID를 받아서 해당 ID의 Todo아이템의 값을 배열방에 업데이트 시켜주는 일을 하게 될겁니다.

...
function App() {
...
  function toggleTodo(id) {
    const newTodos = [...todos]
    const todo = newTodos.find(todo => todo.id === id)
    todo.complete = !todo.complete
    setTodos(newTodos)
  }
...

기존의 todos배열을 newTodos에 복사합니다. 그리고 해당 배열을 돌면서 id가 일치하는 객체를 find함수를 통해서 찾아옵니다. 기존에 저장된 complete값의 반대값으로 변경한 뒤 새롭게 만든 배열을 setTodos를 통해서 업데이트 시켜줍니다. 이 함수는 체크박스를 클릭할때마다 호출이 되야하니 이하 컴포넌트에서 사용할수 있도록 TodoList를 호출할때 todos배열과 함께 인자로 전달하도록 하겠습니다.

<TodoList todos={todos} toggleTodo={toggleTodo}/>

그렇다면 TodoList에서도 추가된 인자를 함수에서 받아야겠지요? 그리고 마찬가지로 Todo컴포넌트가 사용할수 있도록 toggleTodo라는 이름으로 또한번 인자를 전달합니다.

import Todo from './Todo'

export default function TodoList({ todos, toggleTodo }) {
  return (
    todos.map(todo => {
      return <Todo key={todo.id} toggleTodo={toggleTodo} todo={todo} />
    })
  )
}

그러면 마지막으로 Todo.js에서도 함수가 호출될때 인자로 받을수 있게 선언을 해줍니다. 그리고 onChange가 될때마다 toggleTodo함수를 호출해주어야하는데, 이때 todo.id도 함께 호출해야하므로 onChange에서 handleTodoClick이라는 함수를 가르키도록 설정하고 handleTodoClick에서는 이 컴포넌트가 호출된 그 todo의 id를 toggleTodo에 함께 넘겨주면서 호출하면 됩니다.

export default function Todo({ todo, toggleTodo }) {
  function handleTodoClick() {
    toggleTodo(todo.id)
  }
  
  return (
    <div>
      <label>
        <input type="checkbox" checked={todo.complete} onChange={handleTodoClick} />
        {todo.name}
      </label>
    </div>
  )
}

App.js에 보시면 출력부분에 텍스트상자 밑에 해야할일이 몇개나 남았는지 보여주도록 정해놓은 곳이 있습니다. 일단 0으로 임시설정해놓은 것을 이제는 배열에서 complete의 값이 마킹이 안되어있는 것만 개수를 세서 보여주도록 하겠습니다. Array에서 제공하는 filter함수에 complete이 false로 세팅된 것들만 filtering을 하는 함수를 넘겨준뒤 filter에서 반환받은 결과 배열의 개수를 세서 아래와 같이 보여주면 됩니다.

<div>{todos.filter(todo => !todo.complete).length} left to do</div>

브라우저 창에서 잘 되는지 확인해보세요. 아래는 현재까지 진행된 코딩입니다.

App.js

import { useState, useRef } from 'react'
import TodoList from './TodoList'
import { v4 as uuidv4 } from 'uuid';

function App() {
  const [todos, setTodos] = useState([])
  const todoNameRef = useRef()

  function toggleTodo(id) {
    const newTodos = [...todos]
    const todo = newTodos.find(todo => todo.id === id)
    todo.complete = !todo.complete
    setTodos(newTodos)
  }

  function handleAddTodo(e) {
    const name = todoNameRef.current.value
    if (name === '') return
    setTodos(prevTodos => {
      return [...prevTodos, {id: uuidv4(), name: name, complete: false}]
    })
    todoNameRef.current.value = null
  }

  return (
    <>
      <TodoList todos={todos} toggleTodo={toggleTodo}/>
      <input ref={todoNameRef} type="text" />
      <button onClick={handleAddTodo}>Add Todo</button>
      <div>{todos.filter(todo => !todo.complete).length} left to do</div>
    </>
  );
}

export default App;

TodoList.js

import Todo from './Todo'

export default function TodoList({ todos, toggleTodo }) {
  return (
    todos.map(todo => {
      return <Todo key={todo.id} toggleTodo={toggleTodo} todo={todo} />
    })
  )
}

Todo.js

export default function Todo({ todo, toggleTodo }) {
  function handleTodoClick() {
    toggleTodo(todo.id)
  }
  
  return (
    <div>
      <label>
        <input type="checkbox" checked={todo.complete} onChange={handleTodoClick} />
        {todo.name}
      </label>
    </div>
  )
}

localStorage

현재 우리가 만든 앱은 브라우저가 새로고침되면 데이타가 다 날아가버리는 코드였자나요. 그렇게 되면 앱으로써의 의미가 없기때문에 새로고침을 해도 다시 방문해도 이전의 데이타를 그대로 볼수 있는 코드로 업그레이드를 해볼게요. 그러기 위해서는 배열이 업데이트 될때마다 변경된 배열값을 local storage에 저장을 해주는 로직이 필요한데요. handleAddTodo나 toggleTodo등의 함수안에 저장하는 코드를 넣을수도 있지만 그렇게 되면 코드가 너무 지저분해지기 때문에 useEffect라는 기능을 이용해서 todos배열이 변경될때마다 정의된 함수를 호출해서 저장하는 작업을 진행하도록 할게요. 일단 react라이브러리에서 useEffect라는 함수를 사용하겠다고 import 합니다. 그리고 useEffect함수를 호출하는데요. 이때, 함수의 첫번째 인자로는 배열이 변경될때마다 호출할 함수를 넘겨주고, 두번째 인자로는 변경을 감지할 배열의 이름을 넘겨줍니다. 두번째 인자의 데이타타입은 배열인데 이유는 여러개의 변수를 감시하면서 같은 함수를 불러주도록 하기 위함입니다. 하지만 우리는 todos하나만 감시하면 되니까 배열안에 todos만 넣어주도록 합니다. 그리고 첫번째 인자로 넘겨주는 함수 안에서 아래와 같이 local storage에 setItem함수를 이용해서 저장할 local storage의 key와 todos의 값을 넣어주는데 이때 값으로는 문자열이 들어가야하므로 JSON.stringify함수를 통해서 JSON을 문자열로 변환한 뒤 setItem에 넣어 호출합니다.

import { useState, useRef, useEffect } from 'react'
...
const LOCAL_STORAGE_KEY = 'todoApp.todos'
...
function App() {
  const [todos, setTodos] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [])
...
  useEffect(() => {
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
  }, [todos])

그리고 App이 실행되고 todos가 초기화 될때 우리는 useState를 이용해서 []로 초기값을 설정했었는데 그 부분을 위의 코드와 같이 localStorage.getItem함수를 통해서 값을 가져오고 해당 값이 문자열이니 JSON.parse를 통해 객체로 만들어 준뒤 todos에 기본값으로 저장되도록 합니다. 이때 localStorage.getItem으로 아무것도 가져오지 못한 경우에는 []로 초기값을 설정해줍니다. 아래는 최종 코드입니다.

App.js

import { useState, useRef, useEffect } from 'react'
import TodoList from './TodoList'
import { v4 as uuidv4 } from 'uuid';


const LOCAL_STORAGE_KEY = 'todoApp.todos'

function App() {
  const [todos, setTodos] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [])
  const todoNameRef = useRef()

  useEffect(() => {
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
  }, [todos])

  function toggleTodo(id) {
    const newTodos = [...todos]
    const todo = newTodos.find(todo => todo.id === id)
    todo.complete = !todo.complete
    setTodos(newTodos)
  }

  function handleAddTodo(e) {
    const name = todoNameRef.current.value
    if (name === '') return
    setTodos(prevTodos => {
      return [...prevTodos, {id: uuidv4(), name: name, complete: false}]
    })
    todoNameRef.current.value = null
  }

  return (
    <>
      <TodoList todos={todos} toggleTodo={toggleTodo}/>
      <input ref={todoNameRef} type="text" />
      <button onClick={handleAddTodo}>Add Todo</button>
      <div>{todos.filter(todo => !todo.complete).length} left to do</div>
    </>
  );
}

export default App;

TodoList.js

import Todo from './Todo'

export default function TodoList({ todos, toggleTodo }) {
  return (
    todos.map(todo => {
      return <Todo key={todo.id} toggleTodo={toggleTodo} todo={todo} />
    })
  )
}

Todo.js

export default function Todo({ todo, toggleTodo }) {
  function handleTodoClick() {
    toggleTodo(todo.id)
  }
  
  return (
    <div>
      <label>
        <input type="checkbox" checked={todo.complete} onChange={handleTodoClick} />
        {todo.name}
      </label>
    </div>
  )
}

자 이제 브라우저 화면을 새로고침해도, 심지어 닫았다 다시 열어도 TodoList는 입력한 상태 그대로 보존이 됩니다. TodoList를 삭제하고 변경하는 부분은 배운 내용들을 바탕으로 직접 한번 구현해보세요. 끝까지 읽어주셔서 감사합니다.

Resource: https://youtu.be/hQAHSlTtcmY