티스토리 뷰

Projects

[Project] React_TodoList

soobin Choi 2022. 11. 30. 11:42

< state, useState, component, props 설명 예시>

[index.jsx]

import React from 'react';
import {useState} from 'react;
import {ActiveCard} from '@component/Card/ActivityCard';

export default function index(){
	const [todoList, settodoList] = useState([
    	{
        id: 1,
        title: "리액트",
        content="공부하기",
        isDone="false"}
    ]);
    
	const onClick = () => {};

	return(
		<div>
        		<div>Following</div>
			<div>
				{todoList.map((todo) => {
					return <ActivityCard title={todo.title} content={todo.content} isDone={todo.isDone}
                    				/>
				})}
			</div>
		<div>    
	)
}

list가 가지고 있는 todolist의 데이터를 ActivityCard component의 props라는 형태로 넘겨줌

 

[ActivityCard.jsx]

import React from 'react';

export const ActivityCard = (props) => {
	//props 는 title, content, isDone
	//state, props
    
    //취소버튼
    const onClickCancel = () => {
	const newTodoList = props.list.map((todo) => {
    		if(todo.id === props.id){
    			return{...props,isDone: false};
    		}else{
   			return(...todo); 
    		}
    
	});    
    };
    
    //완료버튼
    const onClickDone = () => {};
    
	return(
    	<div className = "card">
            <h1>{props.title}</h1> //props가 가지고 있는 title을 보여줄 거야
            <p>{props.content}<p> //props가 가지고 있는 content를 보여줄거야
            <div>
            	<button>삭제하기</button>

                {props.isDone ? (
                <button onClick={onClickCancel}>취소</button>
                ) : (
                <button onClick={onClickDone}>완료</button>
                )}
            </div>
        </div>
    );
    //이렇게 화면을 그리기 위한 요소들을 return 반환해주는 함수 = component
};

1. 삼항연산자

return안에는 if문 쓸 수 없고 어떤 조건은 삼항연산자로 나타내야 함!
-> ActivityCard가 가지는 isDone이라는 상태에 따라서 텍스트를 다르게 보여줄 수 있음
//삼항연산자 //props가 가지고 있는 isDone상태(true or false)를 보여줄거야
<button>{isDone ? '취소' : '완료'}</button>
/*isDone이 true이면 Done영역에 있으므로 '취소'라는 글자가 보여야 함.
isDone이 false이면 Working영역에 있으므로 '완료'라는 글자가 보여야 함.*/
 
*원래 버튼 이런 형태였음(3번에서 바꿈)

2. state, props

state 나 자신 내가 가진 상태, props 위에 있는 부모의 component에서 받는 상태임
so ActivityCard는 상태가 고정적일 수 없음. (<h1>Title</h1> or <p>Content</p> 일 수 없음)
부모가 어떤 데이터를 내려주느냐에 따라서 title, content, isDone이 다 달라져야 함.
 
즉, title content isDone 값을 외부에서 받아와야 함
ActivityCard component가 부모에게 props를 받음
title, content의 경우 부모에게서 받은 props, 데이터를 보여주기만 하면 됨
<h1>{props.title}</h1> //props가 가지고 있는 title을 보여줄 거야
<p>{props.content}<p> //props가 가지고 있는 content를 보여줄거야

이 데이터는 부모의 list에 담겨있음

{list.map((todo) => {
	return <ActivityCard
    	title={todo.title}
        content={todo.content}
        isDone={todo.isDone}
		/>
})}

3. 취소 버튼 onClickCancel

삭제 버튼은 버튼을 눌렀을 때 데이터를 없애주기만 하면 됨.

but 취소/완료버튼은 화면에 텍스트를 보여줄 때 '취소/완료' 이렇게 다르게 동작해야 함!

{props.isDone ? <button>취소</button> : <button>완료</button>}

(이해하기 쉽도록) 이렇게 바꿨음 -> 우리는 취소 완료 버튼에 각각 함수를 따로 만들어주면 됨

{props.isDone ? (
	<button onClick={onClickCancel}>취소</button>
) : (
	<button onClick={onClickDone}>완료</button>
)}

*따로 함수를 안 만들고 onClick={() => {}} 이렇게 쓸 수도 있음

 

Tip 함수 만들기 전에 구현해야 하는 기능, 동작해야 하는 구연 사항들을 정리해놓기

ex) 취소 버튼을 클릭했을 때 어떤 일이 일어나야 할까? = onClickCancel 함수가 해야할 역할

: isDone이 false여야 Done에서 Working영역으로 올라갈(이동할) 수 있다

isDone=true인 상태에서 취소 버튼을 클릭하면 isDone을 false로 바꿔줘야 함

- 이 때 취소하려는 todo가 어떤 todo인지 찾아야 하고 찾았다면 어떻게 변경할 수 있을지?

: KEY 값 id가 같은 것을 찾고 값을 바꾼 뒤

    const onClickCancel = () => {
    	const todo = props.list.find((todo) => todo.id === props.id); //todo찾기
    };

isDone상태가 바뀌어 업데이트된 배열을 다시 todoList에 새로 갈아 끼워줘야 함

	const [todoList, setTodoList] = useState([
    	{
        id: 1,
        title: "리액트",
        content="공부하기",
        isDone="false"}
    ]);

위의 todo가 이 newTodoList의 몇 번째 순서 index에 있을지 모름

const newTodoList = props.list.map((todo) => {
    if(todo.id === props.id){
    	return{...props.isDone: false};
    }else{
   	return(...todo); 
    }
    
});

so 이렇게 onClickCancel 시 isDone 상태를 바꿔줌

 

[index.jsx] 의 return() 안에 있는 ActivityCard는 todoList가 뿌려준 데이터를 표시하고 있음(todoList의 상태를 쓰고 있음)

todoList의 상태가 업데이트된 경우 반드시 useState()의 setTodoList로 변경해야 함!!!

props.setTodoList(newTodoList);

setTodoList에 우리가 만든 새로운 newTodoList를 넣어주면 버튼을 클릭했을 때 자동으로 업데이트됨

*newTodoList 취소하는 버튼 onclick id값이 같으면 isDone 상태 false 바꿔주고 같지 않으면 그대로 반환하는 함수임

 

✔️ newTodoList라는 데이터를 새로 만들어서 넣어주기만 했는데 사실은 Props.setTodoList(() => {}) 이렇게 함수도 들어갈 있음

props.setTodoList((prevTodoList) => {
	return prevTodoList.map((todo) => {
		if(todo.id === props.id){
			return{
				…props,
				isDone: false,
			};
		}else{
			return todo; // {…todo};와 같음
		}
	});
});

React에 내부적으로 “useState에서 함수형으로 update으로 하게 되면 자동으로 여기에 -setTodoLsit(() 이전 상태 -prev를 넣어줄게요. 자동으로 파라미터를 넣어줄게”라고 설계가 되어있는 것임

prev = 이전값(변수명 아무거나) 이미 여기 안에 todolist 데이터들이 담겨져 있음

# Keyword : useState 함수형 업데이트


< 내가 작성한 코드 질문 & 답변>

Q: onCreate이벤트 발생 시 todo 안의 id값을 1씩 증가시키려면 id+1씩 해야하는 것인지? (id: id+1? or id: setId+1? 아니면 밖으로 빼서 setId(id +1) 이렇게? *const[id, setId] = useState();)

A: 이미 초기 todo id 값을 1, 2 이렇게 줬고 1씩 증가해야 하는 경우 현재 toDos의 길이의 +1을 하는 것이 가장 간단함!

사실 useRef 가 id 관리에 더 적합함. 왜냐하면 useState의 경우 todo 하나를 업데이트할때마다, 새로운 데이터가 추가된 배열 자체를 갈아끼우는 것이므로 데이터가 사라진다고 느껴지지는 않겠지만, 사실 useState의 state는 리렌더링될 때마다 값이 초기화됨. 

# Keyword : useRef

 

여기서 id 는 key 이므로 const id = ''이렇게 밖에 선언해야 하는 것이 아님. id가 식별자 역할을 하는 것인데 그 고유한 값을 가지고 있고 (우리가 바꿔주지 않으면) 그 값을 계속 가지고 있음

  const onCreate = (event) => {
    const todo = {
      id: toDos.length + 1,
      //키 key : 값(변수지정)
      title: title, //키와 변수명 같은 경우 title, 이렇게 생략 가능
      content: content,
      isDone: false,
    };

# Keyword : 객체리터럴

 

Q: <Layout> <Header /> 어떤 역할인지? 단순히 return하는 것 뿐만 아니라 이렇게 값을 넣는 것인지?

A: 이게 바로 Component! 위에서 import하고 파일 분리한 component임. toDos, onRemove, onToggle 이게 props.

해당 component가 받을 수 있는 props를 아래와 같이 써주는 것임. 이렇게 props로 UI를 그릴 수 있음

객체리터럴처럼 생긴 것이 props 키={value}

  return (
    <Layout>
      <Header />
      <Form
        title={title}
        content={content}
        onChange={onChange}
        onCreate={onCreate}
      />
      <List toDos={toDos} onRemove={onRemove} onToggle={onToggle} />
    </Layout>
  );

 

Q: 여기서 ...toDos는 이전의 값들을 품고있는 것이고 뒤의 todo는 button onCreate()로 새롭게 만들어진 것인데 , setTodos로 toDos로 상태를 바꿀 때 이 전개연산자 ...toDos는 옛날 그 데이터들애 새로운 todo를 넣어준다는 의미가 맞는지?

A: 맞습니다! 저희가 일반적으로 배열에 요소를 추가할 때는 push를 하지만 state는 useState로만 사용함! push로 업데이트할 수 없습니다. useState의 상태가 업데이트가 되고 업데이트된 것을 리액트가 알아차려서 리렌더링을 시켜줌(새로운 상태로 화면에 보여지게 됨) 리액트가 state가 바뀌었음을 알아차리려면 반드시 setState()함수를 써야 합니다.

...은 얕은 복사를 해주는 연산자. setToDos를 보면 toDos에 todo를 푸시하는 것이 아니라 아예 새로운 배열을 만드는 것!

그냥 todo만 넣을 수도 있고 어떤 값이든 들어갈 수 있지만 지금은 기존 배열에 새로운 값을 추가하는 것이므로 기존에 있던 배열 ...toDos가 같이 필요한 것임

# Keyword : 불변성

    setToDos([...toDos, todo]);

 

Q: childern 의미?

A: React가 내부적으로 정해놓은 자식 component props 이름. props가 children을 가지고 div태그 안에서 사용해주는 경우,

import React from "react";
import "./style.css";

function Layout({ children }) {
  return <div className="layout">{children}</div>;
}
export default Layout;

layout이 감싸고 있는 아래의 자식들 component가 다 들어가는 것임

  return (
    <Layout>
      <Header />
      <Form
        title={title}
        content={content}
        onChange={onChange}
        onCreate={onCreate}
      />
      <List toDos={toDos} onRemove={onRemove} onToggle={onToggle} />
    </Layout>
  );

Q: App.js 가 실행하는 역할인 것 같은데 component에 있는 기능들을 모아서 pages에 넣었는데 이렇게 한 개만 들어갈 수 있는 건가요?

A: 지금은 페이지가 1개라서 저렇게 하고 있는데 여러 페이지의 component를 다 모아둔 것(다음주 배울 예정)

Q: 그러면 App.js 는 component보다 pages들을 넣는 건가요?

A: 맞습니다.

import React from "react";
import TodoList from "./pages/TodoList";

function App() {
  return <TodoList />;
}
//app 여러 페이지들을 다 모아둠
export default App;

Q: App.css는 초기 react app 화면 css같은데 만약 지우고 싶으면 지워도 되나요?

A: 안쓰시면 지우셔도 됩니다.

Q: style을 파일로 만들어서 jsx랑 폴더로 묶는 경우도 있고 styled component를 쓰는 경우도 있던데 어떻게 하는 건가요?

A: 다음주/다다음주에 배우실 예정입니다.


https://github.com/123456soobin-choi/TodoList_React

 

GitHub - 123456soobin-choi/TodoList_React: project_TodoList_React

project_TodoList_React. Contribute to 123456soobin-choi/TodoList_React development by creating an account on GitHub.

github.com

https://vercel.com/123456soobin-choi/todo-list-react

 

Dashboard – Vercel

The deployment that is available to your visitors.

vercel.com

 

vercel로 배포 완료

< TodoList 전체 코드 >

[components] layout

- Layout.jsx : 전체 레이아웃

import React from "react";
import "./style.css";

function Layout({ children }) {
  return <div className="layout">{children}</div>;
}
export default Layout;

Header, Form, List component가 모두 들어있음

[components] header

- Header.jsx

import React from "react";
import "./style.css";
//같은 폴더 안의 파일이면 ./ 이렇게 씀

function Header() {
  return (
    <div>
      <div className="header-style">
        <div>✨ My Todo List ✨</div>
        <div>React</div>
      </div>
    </div>
  );
}

export default Header;

[components] form

- Form.jsx

import React from "react";
import "./style.css";

//부모로부터 받은 상태, 위에서 내려준 데이터를 보여주는 것
//파라미터 props 안에 title, content, onChange, onCreate이 담겨있는 것
const Form = ({ title, content, onChange, onCreate }) => {
  return (
    <div>
      <div className="add-form">
        <div className="input-group">
          <label className="form-label">제목</label>
          <input
            type="text"
            className="add-input"
            name="title"
            onChange={onChange}
            value={title}
          />
          <label className="form-label">내용</label>
          <input
            type="text"
            className="add-input"
            name="content"
            onChange={onChange}
            value={content}
          />
        </div>
        <button className="add-button" onClick={onCreate}>
          추가하기
        </button>
      </div>
    </div>
  );
};

export default Form;

input-group과 add-button이 있는 Form

onChange로 user가 input에 입력한 값을 가져와서 보여줌

[components] list, todo + [pages] TodoList

- Todo.jsx

import React from "react";
import "./style.css";

function Todo({ todo, onRemove, onToggle }) {
  return (
    <>
      <div className="list-wrapper">
        <div className="todo-container">
          <div>
            <h2 className="todo-title">{todo.title}</h2>
            <div className="todo-content">{todo.content}</div>
          </div>
          <div className="button-set">
            <button
              onClick={() => onRemove(todo.id)}
              className="todo-delete-button button"
            >
              삭제하기
            </button>

            {todo.isDone === false ? (
              <button
                onClick={() => onToggle(todo.id)}
                className="todo-complete-button button"
              >
                완료
              </button>
            ) : (
              <button
                onClick={() => onToggle(todo.id)}
                className="todo-complete-button button"
              >
                취소
              </button>

              // = {item.isDone ? "취소" : "완료"}
            )}
          </div>
        </div>
      </div>
    </>
  );
}

export default Todo;

todo : onCreate()으로 만들어진 새로운 todo 변수를 뜻함

onRemove : todo 카드/리스트들 중에서 내가 '삭제하기'를 선택한 그 todo만 지워줄 거야 - todo.id 고유한 id key로 많은 데이터들 중 내가 삭제하려는 데이터가 어떤 값인지 식별하는 것임

onToggle : isDone이 true인지 false인지에 따라 '취소/완료' 텍스트를 다르게 보여준다.

 

- List.jsx

import React from "react";
import Todo from "../todo/Todo";
import "./style.css";

function List({ toDos, onRemove, onToggle }) {
  return (
    <div className="list-container">
      <h2 className="list-title">Working.. 🔥</h2>
      <div className="list-wrapper">
        {toDos
          .filter((todo) => todo.isDone === false)
          // = .filter((todo) => !todo.isDone)
          .map((todo) => (
            <Todo //Todo Component
              todo={todo}
              key={todo.id}
              onRemove={onRemove}
              onToggle={onToggle}
            />
          ))}
      </div>
      <h2 className="list-title">Done..! 🎉</h2>
      <div className="list-wrapper">
        {toDos
          .filter((todo) => todo.isDone === true)
          //  = .filter((todo) => todo.isDone)
          .map((todo) => (
            <Todo
              todo={todo}
              key={todo.id}
              onRemove={onRemove}
              onToggle={onToggle}
            />
          ))}
      </div>
    </div>
  );
}

export default List;

toDos 배열에 filter()로 조건 isDone true/false에 맞는 값들을 뽑아내고 리스트(카드)들을 보여줌

 

- TodoList.jsx

import { useState } from "react";
import Form from "../components/form/Form";
import Header from "../components/header/Header";
import Layout from "../components/layout/Layout";
import List from "../components/list/List";
import { v4 as uuidv4 } from "uuid";

//input에 제목과 내용을 입력하면 onChangeHandler로 값을 가져와서 보여줌
function TodoList() {
  //제목과 내용 비어있는 초기값을 선언
  const [inputs, setInputs] = useState({
    title: "",
    content: "",
  });

  const { title, content } = inputs;
  const onChange = (event) => {
    const { name, value } = event.target;
    setInputs({
      ...inputs,
      [name]: value,
    });
  };

  //   let [id, setId] = useState(0);

  //toDos 배열의 초기값 넣어주기
  //toDos 상태뿐만아니라 함수 setToDos가 반환되는 것
  //useState로 어떤 상태를 만들었으면(변경했으면( 반드시 전용 업데이트 setToDos 함수로 변경시켜야함
  const [toDos, setToDos] = useState([
    {
      id: 1,
      title: "리액트를 공부하기",
      content: "리액트 기초를 공부해봅시다.",
      isDone: true,
    },

    {
      id: 2,
      title: "리액트를 연습하기",
      content: "리액트 연습을 해봅시다.",
      isDone: false,
    },
  ]);

  console.log(toDos);

  const onCreate = (event) => {
    if (title && content) {
      const todo = {
        id: uuidv4(),
        //키 key : 값(변수지정)
        title: title, //키와 변수명 같은 경우 title, 이렇게 생략 가능
        content: content,
        isDone: false,
      };
      //setToDos에 바뀐 상태를 업데이트 해야함
      //...얕은 복사 아예 새로운 배열을 만드는 것임 todo를 추가하는 것 기존에 있던 것에서 갈아끼우는 것
      //그냥 todo만 넣을 수도 있음 -지금은 '추가'하는 것이므로 아래와 같이 씀
      setToDos([...toDos, todo]);

      setInputs({
        title: "",
        content: "",
      });

      // setId(id + 1);
    } else {
      alert("모두 입력해주세요.");
    }
  };

  //각 리스트의 고유한 값인 id를 가져오기
  //전체리스트들 중에서 어떤 것인지 알아내서 바꿔줘야 함
  const onRemove = (id) => {
    setToDos(toDos.filter((todo) => todo.id !== id));
  };

  //취소하려는 todo가 어떤 todo인지?
  //todo를 만들 때 넣었던 id를 찾아야함!
  //찾았다면 그 어떻게 todo만 isDone을 변경할 수 있을지

  // ...todo(spread operator) : id title content가 안에 담겨있음 바꿔줄 isdone만 보여줌
  const onToggle = (id) => {
    setToDos(
      toDos.map((todo) =>
        todo.id === id ? { ...todo, isDone: !todo.isDone } : todo
      )
    );
  };

  return (
    <Layout>
      <Header />
      <Form
        title={title} //key={value}
        content={content}
        onChange={onChange}
        onCreate={onCreate}
      />
      <List toDos={toDos} onRemove={onRemove} onToggle={onToggle} />
    </Layout>
  );
}

export default TodoList;

title과 content넣은 inputs, 할 일 리스트들을 담을 toDos - useState() 

input - onChange, button - onCreate, onRemove, onToggle 함수 만들기

 

TRUBLESHOOTING

[ERROR] 삭제 추가하다가 콘솔봤더니

Warning: Encountered two children with the same key, `2`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

- toDos인데 todos로 오타내서 생긴 오류

 

[ERROR] 화면에 아무것도 안 보임

Uncaught TypeError: Cannot read properties of undefined (reading 'filter')

- toDos인데 todos 오타내서 생긴 오류

 

+ 배포 후 추가한 기능

- 유효성 검사 input에 비어있는 상태로 추가하기 버튼 누르면 alert 띄우기

uuid import 

import { v4 as uuidv4 } from "uuid";

if문 todo id값에 넣기

    if (title && content) {
      const todo = {
        id: uuidv4(),
        title: title,
        content: content,
        isDone: false,
      };