개인 프로젝트 TODO LIST의 시작입니다!!! 짝짝짝 👏🏻👏🏻👏🏻

이틀이지만 꽤나 긴 여정이 될 것 같습니다.

차근차근 블로그에 기록해가면서 LIST를 쌓아나가겠습니다.

CodeStates에서 배운대로만 만듭니다. 따로 훅스나 다른 기법을 사용하지 않겠습니다.

또한, 상태가 꼭 필요한 부분이 아니면 모든 상태는 App.js에서 관리하겠습니다.


📒 TODO LIST

🤔 THINKING

어떻게 구성해야 할까요? 만들긴 어렵겠지만, 입코딩이라도 해봅시다!

  1. 큰 틀로 전체를 감싸주어야 할 듯 합니다. 앱처럼 보이게요!
  2. 추가 버튼을 클릭하면 저장이되면서, 새로운 리스트가 나와야 겠네요!
  3. 제일 위쪽에 Title을 구성하구요. 오른쪽은 날짜도 있으면 좋겠네요.
  4. Input창에서 입력하면 바로 글씨가 써지고, submit으로 바로 그 자리에 추가해줍시다.
  5. 그리고 바로 다음 입력칸으로 포커스가 넘어가고, 할 일을 작성해야죠.
  6. 각 할 일을 클릭하면 완료한 일을 표시해줘야합니다.
  7. 각 할 일들을 삭제도 해줘야겠죠? 오른쪽 쯤에 버튼하나 추가합시다

1. Base Setting

기본 셋팅을 해봅시다.

먼저 CRA를 이용하여 프로젝트를 생성합니다.

npx create-react-app <프로젝트 이름>

프로젝트를 깔끔하게 구성하기 위해 ESLint와 Prettier를 사용하겠습니다.

마켓플레이스에서 두 개를 설치부터 해주시고 아래로 따라와주셔야해요 :)

package.json에 아래를 붙여넣고 yarn install로 설치합니다.

// package.json
"devDependencies": {
    "eslint-config-prettier": "^6.0.0",
    "eslint-plugin-prettier": "^3.1.0",
    "eslint-plugin-react": "^7.14.3",
    "husky": "^3.0.2",
    "lint-staged": "^9.2.1",
    "prettier": "1.18.2"
  },
  "lint-staged": {
    "*.{js,jsx}": [
      "eslint --fix",
      "git add"
    ]
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  }

다음 .eslintrc파일을 생성하여 아래와 같이 작성합니다.

// .eslintrc
{
  "extends": [
    "eslint:recommended",
    "plugin:prettier/recommended",
    "plugin:react/recommended",
    "prettier",
    "prettier/react"
  ],
  "plugins": ["prettier", "react"],
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "env": {
    "es6": true,
    "browser": true,
    "node": true
  },
  "rules": {
    "prettier/prettier": [
      "error",
      {
        "singleQuote": true,
        "printWidth": 120
      },
      {
        "usePrettierrc": false
      }
    ],
    "no-console": "warn",
    "semi": 2,
    "no-undef": "warn"
  }
}

마지막으로, ESLint와 Prettier 설정을 Setting.json에 추가합니다.

// Setting.json
{
  // ...생략...
  "eslint.autoFixOnSave": true,
  "eslint.packageManager": "yarn",
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "html",
    "typescriptreact"
  ],
  "editor.formatOnSave": true,
  "javascript.format.enable": false,
  "prettier.eslintIntegration": true
}

이제 App.js로 들어가서 Command + s해주시면 자동 정렬이 됩니다!

혹시 코드 작성하시다가 babel관련 Lint에러가 나신다면 아래를 .eslintrc에 추가해보세요!

// .eslintrc
"parser": "babel-eslint",

2. 기본 화면 구성

이번 프로젝트에서는 Sass를 연습해보기 위해서 이후에 scss파일을 만들거에요.

App.js / App.css / index.js / index.css 만 남겨두고 삭제할게요!

그 뒤 간단한 화면 구성을 위해서 내용을 수정합니다!

/* index.css */
body {
  margin: 1rem;
  padding: 0;
  background: #2196f3;
}
// App.js
import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return <div className="App">TODO LIST를 만들어볼거에요!</div>;
  }
}

export default App;
/* App.css */
.App {
  width: 760px;
  margin: 0 auto;
  padding: 2rem;
  background: #fff;
  border-radius: 1em;
  box-shadow: 0 5px 5px rgba(0, 0, 0, 0.3);
}

이제 yarn start로 확인해볼까요? 👇🏻👇🏻👇🏻

image.png


3. Title

간단하게 타이틀부터 만들어보겠습니다.

src폴더에 components 폴더를 생성하고, Title.jsx와 Title.scss를 생성합니다.

Sass를 사용하고 싶으시면 yarn add node-sass를 사용하시면 됩니다!

물론 css를 사용하셔도 만드는 거에 큰 지장은 없지만, 저는 연습을 위해 사용해볼게요!

이번까지만 import와 export를 적고, 이후에는 생략하겠습니다!

// Title.jsx
import React from 'react';
import './Title.scss';

const Title = () => {
  return <div className="title">TODO LIST</div>;
};

export default Title;
// Title.scss
.title {
  padding-bottom: 1rem;
  font-size: 2.5rem;
  font-weight: 700;
  text-align: center;
  border-bottom: 0.8px solid rgba($color: #0000ff, $alpha: 0.2);
}

이제 App.js에서 Title을 불러와주세요.

아래와 같이 나온다면 완성입니다! css는 자유롭게 만들어주시면 됩니다.
image.png


4. Form

어느 정도까지 만들고 포스팅을 해야할 지 애매한데요~ 일단 봅시다!

Form.jsx, Form.scss를 만들고 작성합니다.

갑자기 PropTypes가 나왔죠...?!

각 prop의 타입을 지정해주는건데요, ESLint에서 에러를 주어서 설치했습니다.

아 물론, 타입 지정을 해주는 건 아주 바람직한 코딩방법입니다!

yarn add prop-types를 하시고 추가하시면 됩니다!

TypsScript를 사용할 때까진 타입 지정을 해주어서 오류를 방지해줍시다!

// Form.jsx
import PropTypes from 'prop-types';

const Form = ({ inputRef, onSubmit, onChange, inputText }) => {
  return (
    <form className="form" onSubmit={e => onSubmit(e)}>
      <input className="form-input" ref={input => inputRef(input)} value={inputText} onChange={e => onChange(e)} />
      <input className="form-submit" type="submit" />
    </form>
  );
};

Form.propTypes = {
  inputRef: PropTypes.func.isRequired,
  onSubmit: PropTypes.func.isRequired,
  onChange: PropTypes.func.isRequired,
  inputText: PropTypes.string.isRequired
};

Form의 ref는 App.js에서 초기에 input에 focus를 주기 위해서 사용합니다.

나머지 함수들도 모두 App.js에서 관리하기 위해 prop으로 전달받았습니다!

onChange나 onClick event는 화살표 함수로 함수에 직접 전달해주면 됩니다.

// Form.scss
.form {
  .form-input {
    width: 90%;
    margin: 1.5rem;
    padding: 1rem;
    font-size: 1.2rem;
    color: #2196f3;
    border: 1px solid #eee;
    border-radius: 3px;

    &:focus {
      outline-color: rgba($color: #6cccf8, $alpha: 0.1);
    }
  }
  .form-submit {
    display: none;
  }
}

이제 이 모든걸 관리할 App.js를 수정해줍니다!

간단하게 주석으로 콕콕 설명할게요.

// App.js
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      text: ''
    };
  }

  // TODO : 처음 시작 시에 Input창을 포커스
  componentDidMount() {
    this.textInput.focus();
  }

  // TODO : Input입력 시 값 변화
  onChange = e => {
    this.setState({ text: e.target.value });
  };

  // TODO : input값을 입력하고 submit하면 값을 비웁니다.
  onSubmit = e => {
    e.preventDefault();
    this.setState({
      text: ''
    });
  };

  // TODO : input focus를 위한 함수
  inputRef = input => {
    this.textInput = input;
  };

  render() {
    // state destructuring
    const { text } = this.state;
    // func destructuring
    const { onChange, onSubmit, inputRef } = this;
    return (
      <div className="App">
        <Title />
        <Form inputText={text} inputRef={inputRef} onChange={onChange} onSubmit={onSubmit} />
      </div>
    );
  }
}

render에서 사용할 state와 function을 하나씩 비구조화 할당을 해주시면

코드도 깔끔해지고, 사용할 상태나 함수가 무엇인지 바로바로 확인하실 수 있어요!

이제 화면을 보면...

image.png

화면을 보자마자 Input 포커싱이 들어가는 것을 알 수 있습니다! 👍🏻


5. TodoList & TodoListItem

이제 할 일 목록을 만들어 볼텐데요,

목록과 목록에 들어갈 아이템을 분리합니다.

그래야 목록에 각 할 일들을 담아서 표현해줄 수 있습니다.

안 해줘도 되지만, 나중에 추가 기능 구현에서 편리하도록 합니다.

먼저, App.js에서 상태를 추가합니다. 기본 데이터를 2개 정도 만들어둘게요.

// App.jsx
// ... 생략...
this.state = {
      id: 3,
      text: '',
      checked: false,
      todoList: [{ id: 1, text: '감자 삶아먹기', checked: true }, { id: 2, text: '옷 사러 가기', checked: false }]
    };
...

이제 TodoList.jsx, TodoListItem.jsx 를 작성합니다.

만들어 두었던 todoList배열을 순회하면서 TodoListItem을 생성합니다.

TodoListItem에는 선택 버튼, 텍스트, 삭제 버튼을 미리 생성해 둡니다.

className에 checked를 삼항 연산자로 검사하여 체크 동작을 구현해 줄겁니다.

// TodoList.jsx
const TodoList = ({ todoList }) => {
  return (
    <div className="todo-list">
      {todoList.map(list => (
        <TodoListItem key={list.id} text={list.text} checked={list.checked} />
      ))}
    </div>
  );
};

TodoList.propTypes = {
  todoList: PropTypes.array.isRequired
};
// TodoListItem.jsx
const TodoListItem = ({ text, checked }) => {
  return (
    <div className={`todo-list-item ${checked ? 'checked' : ''}`}>
      <button className="check-button"></button>
      <span className="todo-text">{text}</span>
      <button className="delete-button"></button>
    </div>
  );
};

TodoListItem.propTypes = {
  text: PropTypes.string.isRequired,
  checked: PropTypes.bool.isRequired
};

이제 App.jsx에 컴포넌트를 추가하고 각각의 scss파일을 생성하여 약간의 모양을 수정합니다.

아직 TodoList.scss는 별다른 수정을 하지 않겠습니다.

// App.jsx
// ...생략...
render() {
    // state destructuring
    const { text, todoList } = this.state;
    // func destructuring
    const { onChange, onSubmit, inputRef } = this;
    return (
      <div className="App">
        <Title />
        <Form inputText={text} inputRef={inputRef} onChange={onChange} onSubmit={onSubmit} />
        <TodoList todoList={todoList} />
      </div>
    );
  }
// TodoListItem.scss
@mixin button() {
  width: 20px;
  height: 20px;
  border: none;
  border-radius: 5px;
  transition: all 0.2s;
  outline: none;
}

.todo-list-item {
  margin: 0.3rem;
  padding: 0.5rem;
  .todo-text {
    font-size: 1.3rem;
    margin-left: 12px;
  }
  .check-button {
    @include button;

    background: rgba(0, 0, 0, 0.3);
    &:hover {
      background: rgba(0, 0, 0, 0.2);
    }
  }
  .delete-button {
    @include button;

    font-size: 1.2rem;
    font-weight: 800;
    color: rgba(255, 0, 0, 0.2);
    &:hover {
      color: red;
    }
  }
}

.checked {
  margin-left: 1.5rem;
}

동작을 구현하기 전에 먼저 화면을 확인해 봅시다.

image.png

이제 onSubmit 동작 시 리스트를 추가하면 되겠네요!