또 다른 프로젝트가 시작됐다 ㅎ 저번주 금요일에 리액트 강의가 발제 됐는데 자바스크립트 보다 훨씬 재밌고, 뭔가 이해가 더 잘 되는것 같다(?) 자바스크립트 라이브러리 아닌가..? 강의 듣는데 이해가 잘되서 재밌던 적은 처음이었다 이게 얼마나 갈까?ㅎㅎ

 

 


 

 

UI 구현

  • Todo 추가 하기
  • Todo 삭제 하기
  • Todo 완료/취소 상태 변경하기 (진행중 ↔ 완료)

 

기능 구현

  • 제목과 내용을 입력하고, [추가하기] 버튼을 클릭하면 Working에 새로운 Todo가 추가되고 제목 input과 내용 input은 다시 빈 값으로 바뀌도록 구성해주세요.
  • [삭제하기] 버튼을 클릭하면 Working 또는 Done 에 있는 것과 상관없이 삭제처리가 되도록 해주세요.
  • Todo의 isDone 상태가 true이면, 상태 버튼의 라벨을 취소, isDone이 false 이면 라벨을 완료 로 조건부 렌더링 해주세요.
  • Todo의 상태가 Working 이면 위쪽에 위치하고, Done이면 아래쪽에 위치하도록 구현합니다.
  • Layout의 최대 너비는 1200px, 최소 너비는 800px로 제한하고, 전체 화면의 가운데로 정렬해주세요.
  • 컴포넌트 구조는 자유롭게 구현해보세요.

 

 

🍯 HINT

  • 사용한 hook은 오직 useState
  • 기능 구현을 위해 생성한 함수는 2개 입니다. onChangeHandler , onSubmitHandler
  • 사용한 javascript 내장 메서드는 map, filter 입니다.
  • todo의 initial state는 {id: 0, title: “”, body: “”, isDone: false} 입니다.

 

 


 

 

 

와이어프레임

컴포넌트

TodoTemplate.jsx  화면을 가운데에 정렬시켜주며, 일정 리스트를 보여줍니다. children으로 내부 JSX를 props로 받아 와서 렌더링해줍니다.
TodoAdd.jsx 할 일을 추가하는 컴포넌트 입니다.
TodoList.jsx todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoItem 컴포넌트로 변환하여 보여 줍니다.
TodoItem.jsx 진행중인 할 일 정보를 보여주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줍니다
CurrentDate.jsx 현재 날짜를 알려주는 컴포넌트입니다.
Icons.jsx 배경에 꾸며주는 Sticker 요소를 보여주는 컴포넌트입니다.

 

 

 

 

🐣 프로젝트 생성

1. CRA(Create React App)

아래처럼 성공 메세지가 띄워졌다면 app 생성 성공

yarn create react-app [원하는 프로젝트 이름]

 

 

생성한 App 파일로 Change Directory 하고 yarn을 실행해준다. 그럼 리액트 앱 생성 완료~~ EZEZ~~

cd [생성한 App 이름]
yarn start

 

 

 

2. Vite

yarn create vite [생성할 App 이름] --template react

 

 

생성 완료닷

cd [생성한 App 이름]
yarn
yarn dev

 

Vite로 생성하면 좋은 이유

WebPack을 사용하는 CRA 대신 Esbuild를 사용하는 Vite는 원래 Vue.js 애플리케이션을 위해 만들어졌지만, 
현재는 React, Svelte, Vanilla JS 등 다양한 프레임워크와 라이브러리를 지원한다. 

1. 빠른 콜드 스타트와 HMR(Hot Module Replacement)
2. 속도 측면에서 기존 CRA와는 비교가 되지 않을 정도로 빠르다
3. CRA는 기본적으로 설정을 숨기지만, Vite는 사용자가 필요에 따라 설정을 더 쉽게 조정할 수 있다
4. Go 언어 베이스의 자바스크립트 빌드 툴입니다. CRA가 채택하는 웹팩과 비교할 때, 말이 안되는 수준의 속도

 

 

 

 

 

🐥 UI 구현하기

우선 App.jsx에 리스트 배열을 만들어주고 각 컴포넌트를 배치한다. TodoList가 두개인 것은 진행 중인 리스트 목록과 완료한 리스트 목록을 UI로 구분하기 위함이다.

import React, { useState } from "react";
import "./App.css";
import TodoTemplate from "./component/TodoTemplate";
import TodoAdd from "./component/TodoAdd";
import CurrentDate from "./component/CurrentDate";
import TodoList from "./component/TodoList";

function App() {
  const [todos, setTodo] = useState([
    {
      id: Date.now(),
      contents: "주 2회 클라이밍 가기",
      isDone: false,
    },
    {
      id: Date.now() + 1,
      contents: "식단 관리하기",
      isDone: false,
    },
  ]);


  return (
    <>
      <TodoTemplate>
        <CurrentDate/>
        <TodoList/>
        <TodoList/>
        <TodoAdd/>
      </TodoTemplate>
    </>
  );
}

export default App;
import React, { useState } from "react";
import "./App.css";
import TodoTemplate from "./component/TodoTemplate";
import TodoAdd from "./component/TodoAdd";
import CurrentDate from "./component/CurrentDate";
import TodoList from "./component/TodoList";

 

컴포넌트를 앞으로도 잘 이용할것이라면 import하는 것을 잊지말자. 하면서도 import 오류가 발생하면 아차 싶은 1인..

 

TodoTemplate.jsx

템플릿은 이렇게 구현해주었다. children 으로 내부 JSX를 가져와서 템플릿 안 어떤 컨텐츠든 자유자재로 바뀌도록 해준다. 

import React from 'react'
import Icons from './Icons'

const TodoTemplate = ({children}) => {
  return (
    <div className='template'>
      <div className='wrap'>
        <Icons/>
        {children}
        </div>
    </div>
  )
}

export default TodoTemplate

 

html로 생각해보면 아래와 같다.

<div class = "template">
	<div class = "wrap"></div>
</div>

 

 

Icons.jsx

import React from 'react'
import icon1 from '../assets/icon1.png';
import icon2 from '../assets/icon2.png';
import icon3 from '../assets/icon3.png';
import icon4 from '../assets/icon4.png';

const Icons = () => {
    return (
        <div className='icons'>
            <img src={icon1} alt='todolist' />
            <img src={icon2} alt='icon-heart' />
            <img src={icon3} alt='icon-music' />
            <img src={icon4} alt='icon-sticker' />
        </div>
    )
}

export default Icons

 

 

 

CurrentDate.jsx

현재 날짜를 생성해주고 today에 할당해주었다. 나는 [ 요일, 일자 월 ] 이렇게 배치할거라 요일과 월은 따로 배열에 넣어주었다. 월, 요일 배열 선언을 마쳤다면 포뱃변수에 노출될 날짜를 할당해주면 된다.

import React from 'react'

const CurrentDate = ({todos}) => {
  const today = new Date();
  const month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
  const week = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  let curMonth = month[today.getMonth()];
  let dayWeek = week[today.getDay()];
  const formatDate = `${dayWeek}, ${today.getDate()} ${curMonth}`

  const taskCount = todos.length;

  return (
    <div>
        <h2 className='curdate'>{formatDate}</h2>
        <p className='taskCount'>{taskCount} tasks</p>
    </div>
  )
}

export default CurrentDate

 

 

 

🍯 리액트에서 아이콘 사용하는 방법

아래와 같다. 1️⃣ 명령어 입력 -> 2️⃣ React Icons에 들어간다 -> 3️⃣ 원하는 아이콘 검색 후 import

yarn add react-icons // yarn

https://react-icons.github.io/react-icons/

 

React Icons

 

react-icons.github.io

 

 

 

✨ TodoAdd.jsx

contents로 받은 값으로 새로운 객체를 생성해 기존 객체에 추가해준다. 여기서 스프레드 연산자를 사용한 이유는 불변성을 유지하기 위해서이다. 배열이니 push로 추가해도되지 않을까 할 수 있지만 push한다면 배열에 직접적으로 추가하는 것이기 때문에 값은 변할지 몰라도 메모리 주소는 바뀌지 않는다. 배열 중간에 것을 삭제하면 오류가 날 수 있고, 이것은 불변성 유지가 불가능하다고 볼 수 있다.

 

그래서 스프레드 연산자로 기존 객체를 복사해주고 그 뒤로 생성한 객체를 배열에 넣어주었다. 이러면 불변성을 유지할 수 있다.

// 할 일 추가
  const onInsert = (contents) => {
    const newTodo = {
      id: Date.now(),
      contents: contents,
    };

    setTodo([...todos, newTodo]);
  };

 

onInsert 함수를 만들어주었으니 추가 컴포넌트에서 사용할 수 있도록 props로 받아오자.

<TodoAdd onInsert={onInsert} />

 

추가 컴포넌트에서 기능 구현을 위해 사용한 리액트 Hook은 useState(), useRef(), useEffect()이다. 차례대로 이해한대로 정리를 해보면 아래와 같다.

 

✅ useSatae() : 컴포넌트의 상태를 관리해준다. 가변하는 사용자 입력 값을 동적으로 사용하기 위함.

✅ useRef() : current라는 키값을 지닌 프로퍼티가 생성되고, 값에 어떤 변경을 줄때도 current를 이용한다.
DOM요소에 접근이 가능하면, 불필요한 재렌더링을 하지 않는다는 장점이 있다.

✅ useEffect() : 컴포넌트가 렌더링될때마다 특정 작업을 실행 할 수 있다. (처음 시작할때 / 사라질때 / 업데이트 될때

 

 

 

 

우선, useState로 상태를 선언해준다. 초기값은 빈 문자열로 해준다. 위에 새로운 객체를 추가하는 함수 onInsert를 만들때 contents 값을 받아와 객체를 생성해주도록 했기때문에 onInsert(contents=사용자로부터 받아온 값) 으로 넣어준다. 생성할때 onClick이 아니라 onSubmit를 사용해준 이유는 form 태그를 사용했기 때문이다. 🍯 onSubmit 은 form 태그 내부에 이벤트를 실행할 수 있다. form 제출 이벤트가 발생할 때의 동작을 지정한다. 

const TodoAdd = ({ onInsert }) => {
    const [contents, setContents] = useState('');

    const onChange = (e) => {
        setContents(e.target.value);
    }

    const onSubmit = (e) => {
        onInsert(contents);
        setContents(''); //빈칸
    }


return (
        <>
            <div className='inserPosition'>
                <form className='insertForm' onSubmit={onSubmit}>
                    <p className='modalTitle'>Today</p>
                    <input type='text' value={contents} placeholder='할 일을 입력하고 Enter를 눌러주세요.' onChange={onChange} />
                </form>
            </div>

            <button style={addBtnstyle}}>
                <HiOutlinePlus style={{ color: '#fff', margin: '0' }} />
            </button>
        </>
    )
}

 

이때 오류가 났었는데 console.log로 확인했을 땐 값이 잘 들어가는데 새로고침되면서 값이 안보이는 오류가 생겼다. 이럴때에는 e.preventDefault(); 를 사용해주면 된다.preventDefault 를 통해 이러한 동작을 막을 수 있다.

e.preventDefault();

 

 

추가로 나는 + 버튼을 눌렀을 때 input창을 띄우는 작업을 하고 싶었다. 감이 안와서 튜터님께 여쭤보니 하고싶은 기능을 말로 해보라고 하셨다. "+ 버튼을 눌렀을 때, 인풋창을 띄우고 싶어요."  말 그대로다 처음에 useState로 인풋창의 상태를 false로 지정해주었다.

const [open, setOpen] = useState(false); //인풋창 띄우기(현재 false)

 

그리고 버튼을 눌렀을 때를 코드로 작성해보면 아래와 같다. 난 왜 응용을 못할까.. 🤬 말 그대로 버튼을 눌렀을 때 부정 연산자를 사용해서 상태를 반전(true - false, false - true) 해주는 것이다. 버튼에서 open 상태를 사용할 수 있게 속성으로 가지고 왔다.

<button style={addBtnstyle} onClick={() => setOpen(!open)} open={open}>
<HiOutlinePlus style={{ color: '#fff', margin: '0' }} />
</button>

 

이제 +를 누르면 인풋창이 뜨겠지? 했지만 오류.. 왜그런가 했더니 open 상태가 true가 되었을 때 어떤 화면이 렌더링 되는지 안정해줬기 때문이다. inserPosition 이 클래스를 띄우면 되는데 어찌할까 구글링하다 논리곱연산자로 구현할 수 있는 방법을 찾았다. 

논리곱연산자는(&&) 좌변 true일 때 우변을 실행한다. 아래 코드를 해석해보자면 속성에 open이 true 이면 <div> 태그를 실행해줘~ 다. 

return (
        <>
        {open && (
            <div className='inserPosition'>
                <form className='insertForm' onSubmit={onSubmit}>
                    <p className='modalTitle'>Today</p>
                    <input type='text' ref={inputEl} value={contents} placeholder='할 일을 입력하고 Enter를 눌러주세요.' onChange={onChange} />
                </form>
            </div>
        )}
            <button 
            style={addBtnstyle} 
            className={`rotateBtn ${open ? 'rotated' : ''}`} 
            onClick={() => setOpen(!open)} 
            open={open}>
                <HiOutlinePlus style={{ color: '#fff', margin: '0' }} />
            </button>
        </>
    )

 

실행해보면 오류 없이 잘 나타나는 것을 확인 할 수 있다. 

 

진짜 진짜 마지막으로 +버튼을 누르고 할일을 바로 치려는데 자동 커서가 안되어있어서 인풋창에 커서를 눌렀다가 써야한다는 불편함이 있었다. 그래서 열심히 구글링 다시 시작. 자동커서를 구현하기 위해선 다들 useRef 훅을 사용하는 것 같았다.

const 변수명 = useRef(초기값)

 

userRef를 먼저 생성해주고, input창에 속성으로 ref값을 설정해준다. 

const inputEl = useRef(null);
<input type='text' 
ref={inputEl} 
value={contents} 
placeholder='할 일을 입력하고 Enter를 눌러주세요.' 
onChange={onChange} />

 

버튼을 클릭했을때 input element가 focus 되어야하기 때문에 useEffect를 사용하였다. 이것도 구글링을 통해서 알게된 훅이다. 그러면 랜더링될 때마다 input 엘리먼트가 focus를 유지하는 걸 확인 할 수 있다.

useEffect(() => {
       if(inputEl.current !== null) inputEl.current.focus();  //자동커서
})

 

 


 

UI + 추가기능 완성본

 

 

 

 

 

 

🔗 참조 사이트

 

https://xiubindev.tistory.com/100

 

React Hooks : useEffect() 함수

useEffect 함수는 리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 실행할 수 있도록 하는 Hook 이다. useEffect는 component가 mount 됐을 때, component가 unmount 됐을 때, component가 update 됐을 때, 특정 작업을

xiubindev.tistory.com

https://programming119.tistory.com/100

 

[JS] event.preventDefault() 간단 설명 😊/ preventDefault란?

preventDefault 란? a 태그나 submit 태그는 누르게 되면 href 를 통해 이동하거나 , 창이 새로고침하여 실행됩니다. preventDefault 를 통해 이러한 동작을 막아줄 수 있습니다. 주로 사용되는 경우는 1. a 태

programming119.tistory.com

+ 강의자료

+ Recent posts