하루하루 꾸준히, 인생은 되는대로

리액트

리액트 연습하기!- todo만들기 (타입스크립트와 함께하는)

긤효중 2022. 12. 31. 15:22

먼저 리액트 앱을 타입스크립트와 같이 사용하려면, 

$ npx create-react-app ts-react-tutorial --typescript

명령어를 사용하면 됩니다. (ts-react-tutorial부분은 앱 이름입니다.)

 

전체 폴더 구조

 

스타일링은 styled-components를 사용하였습니다.

 

styled-components는 자바스크립트 파일 안에서 CSS를 사용할 수 있도록 해주는

(CSS-In-JS)라이브러리입니다.

 

styled-components 설치

npm i styled-components

타입스크립트와 함께 사용하려면, 다음의 명령어를 추가로 실행해야 합니다.

npm i -D @types/styled-components

🌕Todo의 타입을 지정해보자

 

할 일 목록(Todo)에는 고유한 id, 실제 할일, 선택되었는지 아닌지

3가지가 필요하다고 생각했습니다.

Todo의 타입을 constants.ts파일에서 새로 생성합니다.

 

src/constants.ts

type Todo = {
  id: string;
  text: string;
  selected: false;
};

export default Todo;

🌕고유한 ID를 어떻게 만들까?

 

Todo의 id는 고유해야 하고, 중복이 없게 만들고 싶었습니다.

 

uuid라이브러리를 사용하면, 고유한 id값을 만들 수 있습니다.

 

UUID

uuid란 Universal Unique Identifier(범용 단일 식별자)의 약자입니다.

uuid 함수를 호출하면, 랜덤으로 생성된 문자열이 만들어집니다.

생성된 랜덤한 문자열 예시

 

UUID를 설치해줍니다

npm install uuid

 

위의 명령어로 에러가 발생한다면, 다음의 명령어를 통해 설치할 수 있습니다.

npm install uuid --legacy-peer-deps

타입스크립트 사용시 추가로 설치해줍니다.

npm install --save @types/uuid --legacy-peer-deps

https://www.npmjs.com/package/uuid

 

uuid

RFC4122 (v1, v4, and v5) UUIDs. Latest version: 9.0.0, last published: 4 months ago. Start using uuid in your project by running `npm i uuid`. There are 50284 other projects in the npm registry using uuid.

www.npmjs.com

 


🌕컴포넌트 : Todotemplate.tsx

Todotemplate.tsx : 위의 그림에서 일정관리 부분에 해당합니다.

할 일 입력부분과, 할 일을 리스트 랜더링 하는 부분을 하위 컴포넌트로 설정했습니다.

 

 

일정 관리부분에 styled-components를 적용해보겠습니다.

먼저 제일 상단에 styled-components를 가져오기 위한 코드를 추가합니다.

import styled from 'styled-components';

 

먼저 전체 소스 코드입니다. (Components/Todotemplate.tsx)

import styled from 'styled-components';

const TodoTemplate = ({ children }: { children: JSX.Element[] }) => {
  return (
    <StyledTodoTemplate>
      <StyledTodoTitle>일정 관리</StyledTodoTitle>
      <StyledTodoContent>{children}</StyledTodoContent>
    </StyledTodoTemplate>
  );
};

const StyledTodoTemplate = styled.article`
  @media (max-width: 1920px) {
    width: 512px;
    margin: 6rem auto;
    border-radius: 4px;
  }

  @media (max-width: 767px) {
    width: 300px;
    margin: 3rem auto;
    border-radius: 2px;
  }
`;

const StyledTodoTitle = styled.section`
  background: #22b8cf;
  color: white;
  font-size: 1.5rem;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 4rem;
  font-weight: 700;
`;

const StyledTodoContent = styled.section`
  background-color: white;
`;

export default TodoTemplate;

 

모든 컴포넌트를 감싸는 부분입니다.

styled-components에서 반응형 웹을 만들떄, 밑처럼 적용할 수 있었습니다.

const StyledTodoTemplate = styled.article`
  @media (max-width: 1920px) {
    width: 512px;
    margin: 6rem auto;
    border-radius: 4px;
  }

  @media (max-width: 767px) {
    width: 300px;
    margin: 3rem auto;
    border-radius: 2px;
  }
`;

다음으로 실제 일정 관리영역입니다.

전체 부분에서 아래의 사진 부분

const StyledTodoTitle = styled.section`
  background: #22b8cf;
  color: white;
  font-size: 1.5rem;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 4rem;
  font-weight: 700;
`;

그리고, 하위 컴포넌트들의 배경색을 정해주는 부분입니다.

배경색은 흰색으로 하였습니다.

const StyledTodoContent = styled.section`
  background-color: white;
`;

🌕컴포넌트 : TodoInsert.tsx

TodoInsert.tsx : 할 일을 입력받는 컴포넌트입니다.

아래 그림의 일정 관리 밑 '할 일을 입력하세요'부분이 이에 해당합니다.

전체 소스 코드

import { MdAdd } from 'react-icons/md';
import styled, { keyframes } from 'styled-components';

export default function TodoInsert({ newtodoRef, onsubmit }:
{ newtodoRef: React.RefObject<HTMLInputElement>; onsubmit: Function }) {
  return (
    <StyledDiv>
      <StyledInput ref={newtodoRef} placeholder="할 일을 입력하세요" />
      <StyledButton type="submit" onClick={() => onsubmit(newtodoRef)}>
        <MdAdd />
      </StyledButton>
    </StyledDiv>
  );
};

const StyledDiv = styled.div`
  display: flex;
  background: #495057;
`;

const fadein = keyframes`
    0% {
      opacity: 0;
    }
    100% {
      opacity: 1;
      transform: none;
    }
`;
const StyledInput = styled.input`
  background: none;
  animation: ${fadein} 1.5s linear;
  outline: none;
  border: none;
  width: 100%;
  display: flex;
  font-size: 1rem;
  &::placeholder {
    color: #dee2e6;
  }
`;
const StyledButton = styled.button`
  background: none;
  outline: none;
  background: #868e96;
  cursor: pointer;
  border: none;
  display: flex;
  justify-content: center;
  align-items: center;
  &:hover {
    background: #adb5bd;
  }
`;

🌕CSS부분

전체 TodoInsert컴포넌트를 감싸는 부분입니다.

배경색과 flex를 지정했습니다.

const StyledDiv = styled.div`
  display: flex;
  background: #495057;
`;

styled-components에서 keyframe으로 애니메이션을 사용할 수 있습니다.

먼저 keyframe을 styled-components에서 가져옵니다.

import styled, { keyframes } from 'styled-components';

그리고, 애니메이션을 나타내는 변수를 생성해줍니다.
fadein이라는 변수로 애니메이션을 다음과 같이 만들 수 있습니다.

const 변수명 = keyframes`

--이부분에 애니메이션 적용--

`

 

const fadein = keyframes`
    0% {
      opacity: 0;
    }
    100% {
      opacity: 1;
      transform: none;
    }
`;

다음으로 입력칸을 꾸며주는 부분입니다.

 

앞서 선언한 애니메이션 변수를 ${변수이름}으로 사용하였고,

Input의 placeholder색상을 지정해주었습니다.

const StyledInput = styled.input`
  background: none;
  animation: ${fadein} 1.5s linear;
  outline: none;
  border: none;
  width: 100%;
  display: flex;
  font-size: 1rem;
  &::placeholder {
    color: #dee2e6;
  }
`;

마지막으로, 입력을 다하고 추가를 할떄, 추가 버튼입니다.

const StyledButton = styled.button`
  background: none;
  outline: none;
  background: #868e96;
  cursor: pointer;
  border: none;
  display: flex;
  justify-content: center;
  align-items: center;
  &:hover {
    background: #adb5bd;
  }
`;

 

TodoInsert컴포넌트는, Input형식의 Ref객체를 받아오고, onsubmit이라는 함수를 받아옵니다.

입력부분에 ref를 달아주고, 추가(+버튼)을 누르면 onsubmit함수가 실행됩니다.

export default function TodoInsert({ newtodoRef, onsubmit }:
{ newtodoRef: React.RefObject<HTMLInputElement>; onsubmit: Function }) {
  return (
    <StyledDiv>
      <StyledInput ref={newtodoRef} placeholder="할 일을 입력하세요" />
      <StyledButton type="submit" onClick={() => onsubmit(newtodoRef)}>
        <MdAdd />
      </StyledButton>
    </StyledDiv>
  );
}

이제, 실제 할 일을 나타내는 컴포넌트를 만듭니다.

react-icons: 여러 아이콘을 리액트에서 사용하기

npm install react-icons --save // npm
yarn add react-icons // yarn

https://velog.io/@ka0son/React-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%95%84%EC%9D%B4%EC%BD%98react-icons-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

React : 리액트 아이콘(react-icons) 적용하기

리액트 아이콘위와 같이 사용하고자 하는 아이콘이름을 {}안에 넣어주면 된다.다시 리액트 홈페이지로 가서 원하는 아이콘을 선택해주면 자동으로 컴포넌트 이름이 복사가 된다.react icons를 impor

velog.io

 

🌕컴포넌트 : TodoListitem.tsx

TodoListitem.tsx : 실제 할 일 하나하나를 나타내는 컴포넌트입니다.

 

먼저 전체 소스 코드입니다.

import { useState } from 'react';
import { MdCheckBox, MdCheckBoxOutlineBlank, MdRemoveCircleOutline } from 'react-icons/md';
import styled, { css, keyframes } from 'styled-components';
import Todo from '../constants';

const TodoListItem = ({ todo, onremove }: { todo: Todo; onremove: Function }) => {
  const [selected, setselected] = useState<Boolean>(todo.selected);
  return (
    <StyledArticle id={todo.id}>
      <StyledSection state={selected}>
        {selected ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <StyledText onClick={() => setselected(!selected)}>{todo.text}</StyledText>
      </StyledSection>
      <StyledSelectedSection onClick={() => onremove()}>
        <MdRemoveCircleOutline />
      </StyledSelectedSection>
    </StyledArticle>
  );
};

const fadein = keyframes`
    0% {
      opacity: 0;
    }
    100% {
      opacity: 1;
      transform: none;
    }
`;

const StyledArticle = styled.article`
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  &:nth-child(even) {
    background-color: #f8f9fa;
  }
  gap: 10px;
`;

const StyledText = styled.div`
  font-weight: 700;
  cursor: pointer;
`;

const StyledSection = styled.section<{ state: Boolean }>`
  display: flex;
  width: 100%;
  align-items: center;
  animation: ${fadein} 2s linear;
  svg {
    font-size: 1.5rem;
    cursor: pointer;
  }
  border-bottom: 1px solid #dee2e6;
  ${({ state }) => state === true && selectedStyle}
`;

const selectedStyle = css`
  svg {
    color: #22b8cf;
  }
  color: #adb5bd;
  text-decoration: line-through;
`;

const StyledSelectedSection = styled.section`
  display: flex;
  align-items: center;
  color: #ff6b6b;
  font-size: 1.5rem;
  cursor: pointer;
  &:hover {
    color: #ff8787;
  }
`;

export default TodoListItem;

 

실제 할일에 해당하는 컴포넌트이므로, 위에서 선언했던, Todo타입을 가져왔고,

styled-components도 마찬가지로 가져왔습니다.

할일 선택시, 체크박스 아이콘, 제거를 표현하는 아이콘도 가져옵니다.(react-icons)

import { useState } from 'react';
import { MdCheckBox, MdCheckBoxOutlineBlank, MdRemoveCircleOutline } from 'react-icons/md';
//아이콘 가져오기
import styled, { css, keyframes } from 'styled-components';
//styled-components
import Todo from '../constants';
//Todo타입

 

TodoListitem은, todoonremove를 받습니다.(TodoList컴포넌트에서 받아옴)

todo실제 할 일을나타내고, onremovetodo를 제거하는 함수입니다.

실제 할 일은 todo타입이고, onremove는 함수이므로, Function을 타입으로 지정했습니다.

const TodoListItem = ({ todo, onremove }: { todo: Todo; onremove: Function })

🌕CSS부분

할 일을 감싸는 부분입니다.

const StyledArticle = styled.article`
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  &:nth-child(even) {
    background-color: #f8f9fa;
  }
  gap: 10px;
`;

 

todo를 선택하면, 밑의 그림과 같이 선택된 표시가 나와야합니다.

할 일이 선택되었는지 아닌지 알기위해 Booelan타입의 useState를 사용합니다.

const [selected, setselected] = useState<Boolean>(todo.selected);

todo타입의  selected의 기본값은 false입니다.

type Todo = {
  id: string;
  text: string;
  selected: false;
};

만약, 할 일을 누른다면, selected의 상태가 바뀌어야 합니다.

 

이를 위해, 먼저 새로운 styled-components를 하나 생성합니다.(section태그)

그리고, 이 section태그는 할 일이 선택되었는지, 선택되지 않았는지를 받아옵니다

선택여부는 Boolean타입으로 설정하였습니다.

(위의 Todo의 selected가 Boolean타입이기 떄문입니다).

const StyledSection = styled.section<{ state: Boolean }>

이제 이 state값이 true라면, 즉 선택된 상태라면, 다음의 코드가 유효합니다.

${({ state }) => state === true && selectedStyle}

state가 참이라면, selectedStyle을, 그렇지 않다면 false가 평가 결과가 될 것입니다.

const StyledSection = styled.section<{ state: Boolean }>`
  display: flex;
  width: 100%;
  align-items: center;
  animation: ${fadein} 2s linear;
  svg {
    font-size: 1.5rem;
    cursor: pointer;
  }
  border-bottom: 1px solid #dee2e6;
  ${({ state }) => state === true && selectedStyle}
`;

이제 selectedStyle을 만들겠습니다. (할 일이 선택된다면 추가로 적용될 스타일)

styled-components에서 css변수를 사용할 수 있습니다.

const selectedStyle = css`
  svg {
    color: #22b8cf;
  }
  color: #adb5bd;
  text-decoration: line-through;
`;

selectedStyle은 css변수이고, svg의 색상을 지정해주었고, 취소선을 그어줍니다.

 

 

글로 하니까, 복잡한데..그림으로 나타내면 다음과 같습니다.


const TodoListItem = ({ todo, onremove }: { todo: Todo; onremove: Function }) => {
  const [selected, setselected] = useState<Boolean>(todo.selected);
  return (
    <StyledArticle id={todo.id}>
      <StyledSection state={selected}>
        {selected ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
        <StyledText onClick={() => setselected(!selected)}>{todo.text}</StyledText>
      </StyledSection>
      <StyledSelectedSection onClick={() => onremove()}>
        <MdRemoveCircleOutline />
      </StyledSelectedSection>
    </StyledArticle>
  );
};

위에서 StyledSection은 선택 영역이고, StyledSelectedSection은 제거 영역입니다.

선택 여부에 따라 다른 아이콘을 랜더링해주고,

(선택 시 MdcheckBox아이콘을, 선택 X시 MdCheckBoxOutlineBlank아이콘을)

 

할 일에 해당하는 문자열을 누르면, state가 바뀝니다.

StyledSelectedSection(제거 영역)을 누르면, 받아온 onremove함수가 실행됩니다.


선택 X시

선택 시


🌕컴포넌트 : TodoList.tsx

TodoList.tsx : 전체 할 일 목록을 리스트 랜더링 해주는 컴포넌트입니다.

 

상단부 부분

import TodoListItem from './TodoListitem';
import Todo from '../constants';
import React from 'react';
import { v4 as uuidv4 } from 'uuid';

TodoListitem이 하나하나의 할 일 목록이므로,

이를 가져왔고, 리스트 랜더링 시, 랜덤한 키 값을 만들기 위해 uuidv4함수도 가져왔습니다.

 

🌞 리스트 랜더링 시, 랜덤 키 값의 중요성

 

리액트에서, 컴포넌트의 속성이나 상태가 바뀔떄마다 render함수를 호출합니다.

render함수는 새 리액트 요소를 반환하고, 기존 요소 트리와 비교해, 새로운 변경점에서만

리랜더링을 수행합니다.

 

리액트에서 두 트리를 비교하기위해 key속성을 사용하며, 

자식 요소들을 반복적으로 랜더링하는 과정에서, key를 사용합니다.

 

즉, 두 트리를 비교할 떄 key를 비교하고, key가 달라지면 랜더링도 다시합니다.

key prop에는 item의 id를 지정하거나, react uid 라이브러리를 이용해 고유한 key를 지정해주어야 합니다.

 

https://velog.io/@dev-redo/React-React%EC%97%90%EC%84%9C-list%EB%A5%BC-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%8B%9C-key-prop%EC%97%90-index%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94

 

[React] React에서 list를 렌더링 시 key prop에 index를 사용하지 마세요!

해당 포스팅은 React에서 list를 렌더링 시 key prop에 index를 지양해야 하는 이유에 대해 서술한다.

velog.io

할 일을 제거하는 onremove함수

 const onRemove = (text: string) => {
    setTodo(todo.filter((eachtodo) => eachtodo.text !== text));
  };

전체 소스 코드

import TodoListItem from './TodoListitem';
import Todo from '../constants';
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
const TodoList = ({ todo, setTodo }: { todo: Todo[]; setTodo: Function }) => {
  const onRemove = (text: string) => {
    setTodo(todo.filter((eachtodo) => eachtodo.text !== text));
  };

  return (
    <>
      {todo.map((eachtodo) => (
        <TodoListItem
          key={uuidv4()}
          todo={{
            id: eachtodo.id,
            text: eachtodo.text,
            selected: eachtodo.selected,
          }}
          onremove={() => onRemove(eachtodo.text)}
        />
      ))}
    </>
  );
};

export default TodoList;

마지막 App.tsx

import './App.css';
import TodoTemplate from './Components/TodoTemplate';
import TodoInsert from './Components/TodoInsert';
import TodoList from './Components/TodoList';
import React, { useState, useRef, useCallback, useMemo } from 'react';
import Todo from './constants';
import { v4 as uuidv4 } from 'uuid';

function App() {
 
  const [todo, setTodo] = useState<Todo[]>([]);
  const newtodoRef = useRef<HTMLInputElement>(null);

  const onSubmit = (newtext: React.RefObject<HTMLInputElement>) => {
    if (newtext.current === null) {
      return;
    }
    const newtodo: Todo = {
      id: uuidv4(),
      text: String(newtext.current.value),
      selected: false,
    };
    setTodo([...todo, newtodo]);
    newtext.current.value = '';
  };

  return (
    <>
      <TodoTemplate>
        <TodoInsert newtodoRef={newtodoRef} onsubmit={onSubmit} />
        <TodoList todo={todo} setTodo={setTodo} />
      </TodoTemplate>
    </>
  );
}

export default React.memo(App);

https://github.com/khj0426/Today-I-learnded/tree/main/React/my-app

 

GitHub - khj0426/Today-I-learnded: 오늘 배운 것들을 정리하는 공간입니다.

오늘 배운 것들을 정리하는 공간입니다. . Contribute to khj0426/Today-I-learnded development by creating an account on GitHub.

github.com