[react]처음부터 다시 react-todolist

Grace·2021년 4월 29일
25

react

목록 보기
1/6
post-thumbnail
post-custom-banner

백지상태에서 다시 그려보는 react

작업을 몇개월동안 쉬면서, 간단한 코드작성조차 다 잊어버린 내 멍청한 두뇌
그래서 다시 초심으로 돌아가서 ToDoList 만들기를 했다.
그래도 html,css,js를 작업한 이후라서 이해의 속도는 빨라졌으나,
이전에도 이해하지 못하고 넘어갔던 부분들에 또 막혔다.
하지만 이번에는 제대로 이해하고 넘어가도록..!

안다고 생각하는건 제대로 아는게 아니야 🧐

📌 todolist 만들기 : https://codecrafting.tistory.com/
📌 map함수 : https://www.zerocho.com/category/JavaScript/post/5acafb05f24445001b8d796d
📌 useRef : https://react.vlpt.us/basic/10-useRef.html
참고하여 작업했습니다 :)

1. ToDoList 레이아웃 잡기

머리로만 정리해서 작업을 시작하기보다는, 눈으로 구조화 시킨 후에 작업을 진행했다.
대충, 그려보자면 이런식으로.

1. 스타일링

전체적인 css 스타일링은 styled-components로 해주기로 하고,
눈의 피로도를 낮추기 위해 App의 배경색도 어두운 색으로 변경해주었다.

import styled, {createGlobalStyle} from 'styled-components';
...
<Fragment>
  <GlobalStyle />
  <Container>
  	<TodoTemplate />
  </Container>
</Fragment>
...
const GlobalStyle = createGlobalStyle`
  body {
    background-color: #425364
  }
`;

나는 기능작업을 하기 앞서서 어느정도 구도가 잡혀져 있는 상태까지 만들어 두는게
맘이 편하기 때문에, flex를 적절히 사용해서 가운데정렬도 시켜주었다.
다만, display: flex가 어떤 상황에서 제대로 작동하는지
공부를 더 해야 할 것 같다. 이것땜에 시간을 좀 낭비한..

2. component 역할

TodoTemplate (부모 component )

import React from "react";
import styled from "styled-components";

import TodoTitle from "./TodoTitle";
import TodoInsert from "./TodoInsert";
import TodoItemList from "./TodoItemList";

function TodoTemplate() {
  return (
    <Container>
      <TodoTitle>TODO LIST - eassy</TodoTitle>
      <TodoInsert />
      <TodoItemList />
    </Container>
  );
}

state값을 TodoItemList에 props로 보내줌으로서 리스트로 뽑아내게 한다.

TodoTitle

ToDoList의 Title에 해당하는 부분.
복잡한 코드는 없지만, 나중을 위해서 component를 세분화해보기로 했기에
children으로 받아서 보여주기로 했다.

import React from "react";
import styled from "styled-components";

function TodoTitle(props) {
  return <Container>{props.children}</Container>;
}

‼️ props를 받아오는데에 있어서, 입력하는 방법을 잊었었다ㅠㅠ
props를 함수의 파라미터로 받아와서 사용하는데,
위와 동일하게 작동하지만 다르게 작성하는 방법은

function TodoTitle({children}) {
  return <Container>{children}</Container>;
}

TodoInsert

할 일을 목록에 추가하기 위한 input창이 있는 component.
input창에서 text가 쓰이는 동안에는 부모 component와는 관련이 없다가,
ADD버튼을 누르는 순간 list에 추가가 되면서 props가 필요해진다.

ToDoList를 만드는 방법은 다양하게 있지만,
나는 그중에서도 form태그를 사용해서 submit을 해보기로 했다.

import React from "react";
import styled from "styled-components";

function TodoInsert() {
  return (
    <Container>
      <form>
        <TextInput
          type="text"
          name="text"
          placeholder="Hey, input here -"
        />
        <AddButton type="submit">
          ADD
        </AddButton>
      </form>
    </Container>
  );
}

TodoItemList

input창에서 입력된 내용이 list로 나열되는 component
title 다음으로 제일 간단한 코드가 사용된다.
state도 필요없고, 그냥 props로 값을 받아서
Item에 쌓인 내용을 출력해주기만 하면 되기 때문!

import React from "react";
import styled from "styled-components";

import TodoItem from "./TodoItem";

function TodoItemList() {
  return (
    <Container>
        <TodoItem/>
    </Container>
  );
}

TodoItem

ItemList에서 받은 props들을 사용해서 뽑아온 데이터들.
이 component에서는 list에서 완료한 내용들을 check하거나 delete하는 버튼들에도 기능을 추가해줄 예정.

import React from "react";
import styled from "styled-components";

function TodoItem() {
  return (
    <Container>
      <DoneButton>
        {checked ? <CheckBox src="./img/check.png" /> : null}
      </DoneButton>
      <Topic
        style={{
          textDecoration: checked ? "line-through" : null,
          color: checked ? "#ccc" : "#000",
        }}
      >
        {text}
      </Topic>
      <DeleteButton>
        <DeleteImg src="./img/trash.png" />
      </DeleteButton>
    </Container>
  );
}

이렇게 작업하면

2. 기능 구현하기

어느정도 css 작업까지 마쳤다면, 이제 제 기능을 보여주어야 한다.
기능구현은 js를 아직 익숙하게 해내지 못하는 나에게는 벅찬부분이지만,
수십번의 리서치와 시도를 통해서 구현해내기에 성공했다 😂

1. TodoTemplate에서의 state(todos) 상태 사용하기

추후에 추가될 항목들에 대한 상태는 모두 TodoTemplate에서 관리한다.
때문에 useState를 사용하여 todos라는 상태를 정의하고,
이를 TodoItemListprops로 전달한다.

function TodoTemplate() {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: 'todolist 레이아웃 잡기',
      checked: true,
    },
    {
      id: 2,
      text: 'todolist css 스타일링 하기',
      checked: true,
    },
    {
      id: 3,
      text: 'todolist 기능 구현하기',
      checked: false,
    },
  ]);

  return (
    <Container>
      <TodoTitle>TODO LIST - eassy</TodoTitle>
      <TodoInsert />
      <TodoItemList todos={todos} />
    </Container>
  );
}

todos배열을 props로 넘길때에는,
안에 들어있는 id, text, checked의 값이 함께 전달되기 때문에
이 값들을 TodoItem으로 변환해서 랜더링 할 수 있게된다.

function TodoItemList({ todos }) {
  return (
    <Container>
      {todos.map((todo) => (
        <TodoItem
          todo={todo}
          key={todo.id}
        />
      ))}
    </Container>
  );
}

여기서는 props로 받은 todos 배열을 js내장함수인 map()함수를 사용하여
TodoItem으로 이루어진 새로운 배열로 변환하여 랜더링 해준다.
map을 이용하여 component로 변환할 때는 key props를 전달해 주어야 하는데 ,
이때 각 todo 객체의 고윳값이 id를 전달한다.


💡 map( ) 함수에 대해 짚고 넘어가기
js 배열 객체의 내장함수인 map 함수의 사용으로, 반복되는 component를 랜더링 할 수 있다. map은 파라미터로 전달 된 함수를 사용하여 배열 내 각 요소를 원하는 규칙에 따라서 변환한 후 기존 배열은 수정하지 않고 새로운 배열로 생성한다.

  • 사용법
    원본배열.map((요소, index, 원본배열) => {return 요소});
  • 예제
 const array = [1, 4, 6, 9];

 const newArray = array.map(x => x * 2);

 console.log(newArray); // [2, 8, 18, 32]

map의 기본 원리는 반복문을 돌며 배열 안의 요소들을 1대1로 묶어주는 것이다.
대신 기존 객체를 건드리지 않고 공유하게 되는 메서드이다.


map을 통해 리스트가 새롭게 생성되었을 때 보여질 TodoItem에서의 UI는

function TodoItem({ todo }) {
  const { id, text, checked } = todo;
  // TodoItemList에서 props로 넘긴 값들

  return (
    <Container>
      <DoneButton>
        {checked ? <CheckBox src="./img/check.png" /> : null}
      </DoneButton>
      <Topic
        style={{
          textDecoration: checked ? "line-through" : null,
          color: checked ? "#ccc" : "#000",
        }}
      >
        {text}
      </Topic>
      <DeleteButton>
        <DeleteImg src="./img/trash.png" />
      </DeleteButton>
    </Container>
  );
}

checkedtrue면 체크이미지가 나타나지면서 동시에
할일 목록에 적힌 항목의 글씨가 연해지고 가운데실선이 생기도록
작업을 해주었다.

2. 항목추가기능 구현

이제 component들을 연결해주었으니 기능을 추가해주면 된다.
우선, 일정항목을 추가할 수 있는 기능을 구현하려 한다.
이 기능을 구현하기 위해서는

  • TodoInsert component에서의 인풋상태 관리
  • TodoTemplate component의 todos 배열에 새로운 객체를 추가하는 함수 생성

TodoInsert 에서 input창에 텍스트가 입력될 때 마다 state값인 content가 들어갈 수 있게 설정해 준 후, 이 설정값이 todos배열에 추가되도록 TodoTemplateonSubmit 함수를 추가해준다.

function TodoInsert(props) {
  
  const [content, setContent] = useState("");

  // 이벤트가 발생할 때마다(글자가 하나씩 입력될 때 마다) 변화를 감지
  const handleChange = (e) => {
    setContent(e.target.value);
  };

  return (
    <Container>
      <form>
        <TextInput
          type="text"
          name="text"
          placeholder="Hey, input here -"
          value={content}
          onChange={handleChange}
	  autoFocus
        />
        <AddButton
          type="submit"
          onClick={handleSubmit}
        >
          ADD
        </AddButton>
      </form>
    </Container>
  );
}

TodoInsert에서 새로운 항목을 받을 준비가 되었다면,
TodoTemplate에서는 onSubmit을 사용해서 todos 배열에
새 객체를 추가해주는 작업을 한다.

function TodoTemplate() {
  const [todos, setTodos] = useState([]);

  const nextId = useRef(0);

  const handleSubmit = (text) => {
    // setTodos([...todos, text]);
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    setTodos(todos.concat(todo));
    nextId.current += 1; // nextId를 1씩 더하기
  };

  return (
    <Container>
      <TodoTitle>TODO LIST - eassy</TodoTitle>
      <TodoInsert onSubmit={handleSubmit} />
      <TodoItemList todos={todos} />
    </Container>
  );
}

이때 만든 onSubmit 함수에서는 새로운 객체를 만들 때마다
id + 1 해주어야 하는데, 이 작업을 useRef라는 hook을 사용해준다.
useRef()를 사용해서 Ref객체를 만들고, 이 객체를
id값은 랜더링 되는 정보가 아니기 때문에, 따로 변수를 만들어서
새로운 항목을 만들 때 참조만 해준다.


💡 useRef에 대해 짚고 넘어가기
함수형 component에서 ref를 쉽게 사용하기 위해 쓰이는 hook이다.

ref가 뭐야 ❓
리액트 프로젝트 내부에서 특정 DOM에 이름을 다는 방법으로,
전역적으로 작동하지 않고 컴포넌트 내부에서만 작동함

useRef는 두 가지 방법으로 쓰이곤 하는데

  1. 특정 DOM(태그) 선택하기
    -> 쉽게 말해서 ref를 사용하여 어떤 일을 시킬 수 있다는 것이다.
    특정 엘리먼트의 크기를 가져와야 한다던지, 스크롤바 위치를 가져오거나 설정해야된다던지, 또는 포커스를 설정해줘야 할 경우에 ref를 사용한다.
    실제 사용해본 코드로 예를 들면,
    useEffect와 함께 사용해서,
    랜더링이 되었을 때 커서가 인풋에 포커스 되는 결과를 얻을 수 있다.
function TodoInsert(props) {
  const [content, setContent] = useState("");
  const ref = useRef();
  
  ...
  
  useEffect(() => {
    ref.current.focus();
  }, []);

  return (
    <Container>
      <form onSubmit={handleSubmit}>
        <TextInput
          ref={ref}
          ...
        />
        <AddButton
          type="submit"
          ...
        >
          ADD
        </AddButton>
      </form>
    </Container>
  );
}

useRef()를 사용하여 Ref 객체를 만들고,
이 객체를 선택하고 싶은 DOM 에 ref 값으로 설정해준다.
그러면, Ref 객체의 .current 값은 원하는 DOM 을 가르키게 됩니다.

추가로,

useEffect에 대해 간단히 말하자면,
리액트 component가 랜더링 될 때 마다 특정 작업을 수행하도록 설정하는 hook
-> 클래스형 component의 componentDidMount와 componentDidUpdate를 합친 형태로 보아도 됨

위의 코드를 참고해보면,
useEffect에서 설정한 함수(input에 포커스 되는 것)를 component 첫 화면에
랜더링 될 때만 실행하고, 업데이트 이후에는 실행하지 않게 된다.

  1. component 안에서 조회 및 수정할 수 있는 로컬변수를 관리할 수 있다.
    여기서의 로컬변수란, 렌더링과 상관없이 바뀔 수 있는 값을 말한다.

리액트 component에서의 상태는 state를 바꾸는 함수를 호출 후
리렌더링이 된 후 업데이트 된 상태를 확인할 수 있으나,
useRef로 관리하는 변수는 설정 후 바로 조회가 가능하다.
-> 실제 사용해본 코드로 예를 들면,

function TodoTemplate() {
  const [todos, setTodos] = useState([]);

  const nextId = useRef(0);
  // .current의 기본값을 0으로 지정

  const handleSubmit = (text) => {
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    setTodos(todos.concat(todo));
    nextId.current += 1;
  };

  return (
    <Container>
      <TodoTitle>TODO LIST - eassy</TodoTitle>
      <TodoInsert onSubmit={handleSubmit} />
      <TodoItemList todos={todos} />
    </Container>
  );
}

이때, ref 안의 값이 바뀌어도 컴포넌트는 렌더링이 일어나지 않는다.
그렇기 때문에 렌더링과 관련이 없는 값을 관리할 때 사용해야 한다.

또한 useRef() 를 사용 할 때 파라미터를 넣어주면,
이 값이 .current 값의 기본값이 되며,
이 값을 수정하거나 조회할 경우에 .current값을 사용하면 된다.


이제 만들어진 onSubmit함수를
추가기능을 담당하는 TodoInsertprops로 전달한다.

function TodoInsert(props) {
  const [content, setContent] = useState("");
  const ref = useRef();

  const handleChange = (e) => {
    setContent(e.target.value);
  };

  const handleKeyPress = (e) => {
    if (e.key === "Enter") {
      handleSubmit();
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault(); // onSubmit 이벤트는 브라우저를 새로고치기 때문에 막아주기
    if (!content) return;
    // 만약 input 창이 빈채로 submit을 하려고 할 땐 return시키기
    props.onSubmit(content);
    setContent("");
    // submit을 한 후에는 input 창을 비우기
  };

  useEffect(() => {
    ref.current.focus();
  }, []);

  return (
    <Container>
      <form onSubmit={handleSubmit}>
        <TextInput
          ref={ref}
          type="text"
          name="text"
          placeholder="Hey, input here -"
          value={content}
          onChange={handleChange}
        />
        <AddButton
          type="submit"
          onClick={handleSubmit}
          onKeyPress={handleKeyPress}
        >
          ADD
        </AddButton>
      </form>
    </Container>
  );
}

사실 처음에 input과 button만을 이용해서 만들어 보려 했는데,
도저히 onSubmit을 완성시키기가 어려워서
리서치를 하던 도중, form을 사용한걸 보고 예전에 사용했던게 문뜩 떠올랐다.
onSubmit을 하기 위해서는 form에 설정해주어야 한다는걸 잊지말자..!

TodoTemplate에서 props로 받은 onSubmit을 사용하여 handleSubmit 함수를 만든다.
ADD가 클릭되어서 이 함수가 호출되면, props로 받아온
onSubmit함수에 현재 content값을 파라미터로 넣어 호출하면서
현재의 content값은 초기화 시키게 된다.

이로서 항목추가기능 구현 완료!!

3. 항목 삭제기능 구현

list에서 중간 항목을 삭제하더라도 순서가 변하지 않도록,
배열의 불변성을 지키면서 배열 원소를 제거해하기 때문에
배열 내장함수인 filter를 사용해서 삭제기능을 구현했다.


💡 filter에 대해 짚고 넘어가기
filter함수는 배열에서 특정 조건을 만족하는 값들만 따로 추출하여
새로운 배열을 만드는 함수다.

조건을 확인해주는 함수를 파라미터로 넣되, 그 함수는 true / false 값을 반환해야 한다.
여기서 true를 반환하는 경우에만 새로운 배열에 추가된다.
-> 사용 예제로는

const todos = [
 {
   id: 1,
   text: '자바스크립트 입문',
   done: true
 },
 {
   id: 2,
   text: '함수 배우기',
   done: true
 },
 {
   id: 3,
   text: '객체와 배열 배우기',
   done: true
 },
 {
   id: 4,
   text: '배열 내장함수 배우기',
   done: false
 }
];

const filterResult = todos.filter(todo => todo.done === false);
// const filterResult = todos.filter(todo => !todo.done); 과 동일
console.log(filterResult);
//[
//  {
//    id: 4,
//    text: '배열 내장 함수 배우기',
//    done: false
//  }
//];

우선 TodoTemplate에서 삭제기능을 담당할 onRemove함수를 생성해준 후,
id를 파라미터로 받아와 같은 id를 가진 항목을 todos 배열에서
지워주면(filter) 된다.

function TodoTemplate() {
  const [todos, setTodos] = useState([]);

  const nextId = useRef(0);

  const handleSubmit = (text) => {
    // setTodos([...todos, text]);
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    setTodos(todos.concat(todo));
    nextId.current += 1;
  };

  const onRemove = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <Container>
      <TodoTitle>TODO LIST - eassy</TodoTitle>
      <TodoInsert onSubmit={handleSubmit} />
      <TodoItemList todos={todos} onRemove={onRemove} />
    </Container>
  );
}

생성된 onRemove함수는 TodoItemListprops로 전달되며

function TodoItemList({ todos, onRemove }) {
  return (
    <Container>
      {todos.map((todo) => (
        <TodoItem
          todo={todo}
          key={todo.id}
          onRemove={onRemove}
        />
      ))}
    </Container>
  );
}

받아온 props를 다시 TodoItem에 그대로 전달해준다.
삭제버튼에 onRemove함수에 현재 자기자신이가진 id값을 넣어서
삭제 함수를 호출하도록 설정하면 끝!

function TodoItem({ todo, onRemove, onToggle }) {
  const { id, text, checked } = todo;

  return (
    <Container>
      <DoneButton>
        {checked ? <CheckBox src="./img/check.png" /> : null}
      </DoneButton>
      <Topic
        style={{
          textDecoration: checked ? "line-through" : null,
          color: checked ? "#ccc" : "#000",
        }}
      >
        {text}
      </Topic>
      <DeleteButton onClick={() => onRemove(id)}>
        <DeleteImg src="./img/trash.png" />
      </DeleteButton>
    </Container>
  );
}

4. 항목 체크기능 구현

list에서 할 일을 체크할 수 있는 기능을 구현하는 것은
방금 했던 삭제 기능과 비슷하게 작업하면 된다.

TodoTemplate에서 onToggle함수를 만들어 propsTodoItemList에 넘겨준다.
이때 또 다시 배열 내장함수인 map함수가 사용된다.

function TodoTemplate() {
  const [todos, setTodos] = useState([]);

  const nextId = useRef(0);

  const handleSubmit = (text) => {
    // setTodos([...todos, text]);
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    setTodos(todos.concat(todo));
    nextId.current += 1;
  };

  const onRemove = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  const onToggle = (id) => {
   setTodos(
     todos.map((todo) => {
      // if문 사용 시 코드
      // if (todo.id === id) {
      //   todo.checked = !todo.checked;
      // }
      // return todo;
      return todo.id === id ? { ...todo, checked: !todo.checked } : todo;
      })
    );
  };

  return (
    <Container>
      <TodoTitle>TODO LIST - eassy</TodoTitle>
      <TodoInsert onSubmit={handleSubmit} />
      <TodoItemList todos={todos} onRemove={onRemove} onToggle={onToggle} />
    </Container>
  );
}

항목은 그대로 유지하며 특정 배열원소(checked)만 업데이트 하는 상황이기에,
map 함수를 사용하여 특정 id를 갖고 있는 객체의 checked값을 반전시켜주었다.

onToggle함수에서 삼항연산자를 사용하여
todo.id === id일때는 정해준 규칙대로 새로운 객체를 생성하지만,
id 값이 다를 때에는 변화를 주지 않고 처음 받아온 그대로의 배열을 보여주게 된다.
그렇기에 map함수를 사용하여 만든 배열에서 변화가 필요한 원소만 업데이트되고
나머지는 그대로 남아있게 되는 것이다.

마지막으로 받아온 props를 다시 TodoItem에 그대로 전달해준다.

function TodoItem({ todo, onRemove, onToggle }) {
  const { id, text, checked } = todo;

  return (
    <Container>
      <DoneButton onClick={() => onToggle(id)}>
        {checked ? <CheckBox src="./img/check.png" /> : null}
      </DoneButton>
      <Topic
        style={{
          textDecoration: checked ? "line-through" : null,
          color: checked ? "#ccc" : "#000",
        }}
      >
        {text}
      </Topic>
      <DeleteButton onClick={() => onRemove(id)}>
        <DeleteImg src="./img/trash.png" />
      </DeleteButton>
    </Container>
  );
}

체크버튼에 onToggle함수를 추가한 후 id를 넣어주면
체크기능까지 구현이 끝나게 된다!

profile
쉽게 사는건 재미가 없더군요, 새로 시작합니다🤓
post-custom-banner

0개의 댓글