안 쓰던 블로그

To Do List 웹 프로젝트_UI 디자인 및 구성(css사용) 본문

Web/React

To Do List 웹 프로젝트_UI 디자인 및 구성(css사용)

proqk 2020. 7. 16. 18:02
반응형

컴포넌트 계획

1. PageTemplate

이 컴포넌트는 UI의 전체적인 틀을 잡는다

흰색 배경+그림자 위에 '일정 관리'라는 타이틀 보여 주고, 타이틀 아래에는 children값으로 아래 들어갈 컴포넌트들을 넣는다

 

2. TodoInput

이 컴포넌트는 일정을 추가할 때 사용하는 input 컴포넌트로, 버튼을 누르는 버튼 이벤트가 들어갈 것이다

 

3. TodoItemList

TodoItem 컴포넌트 여러 개를 렌더링 하는 역할이다

 

4. TodoItem

일정을 렌더링하는 컴포넌트다

클릭하면 체크되면서 전체에 줄을 긋는다

지우기 버튼을 누르면 일정을 화면에서 없앤다

 

1. PageTemplate 컴포넌트 생성

먼저 스타일링부터 한다

src/components/PageTemplate/PageTemplate.scss를 만든다

.todo-list-template{
    margin-top: 5rem;
    margin-left: auto;
    margin-right: auto;
    width: 500px;
    background: white;
    box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
    padding-top: 2rem;
}

.title{
    padding: 2rem;
    font-size: 3rem;
    text-align: center;
    font-weight: 100;
    background: #ffa94d;
    color: white;
}
.form-wrapper{
    text-align: center;
    font-size: 4rem;
    font-weight: 300;
    margin: 0;
    border-bottom: #ffa94d;
}

.todos-wrapper{
    margin-top: 2rem;
}

색 참고: https://yeun.github.io/open-color/

 

다음 PageTemplate.js를 만든다

import React from 'react';
import './PageTemplate.scss';

const PageTemplate = ({form, children}) => {
    return(
        <main className="todo-list-template">
            <div className="title">
                 오늘 할 일
            </div>
            <section className="form-weapper">
                {form}
            </section>
            <section className="todos-wrapper">
                {children}
            </section>
        </main>
    );
};

export default PageTemplate;

 

함수형 컴포넌트지만 파라미터를 (props)=>{...}가 아닌 ({form, children})=>{...}으로 비구조화 할당을 하였다

즉 form과 children이라는 props 두 가지를 받게 된다

 

form은 나중에 JSX형태로 전달해서 인풋과 버튼 컴포넌트를 렌더링 할 때 사용하고,

children도 JSX형태로 wrapper 컴포넌트를 만들어 넣을 것이다

 

index.css에서 배경색을 회색으로 만들고

@import 'utils';
body{
    background: $oc-gray-1;
    margin: 0px;
}

 

App.js에서 렌더링 해 준다

import React, { Component } from 'react';
import PageTemplate from './PageTemplate';

class App extends Component {
    render() {
        return (
            <PageTemplate>
                일정관리
            </PageTemplate>
        );
    }
}

export default App;

 

이제 인풋과 버튼이 담긴 form을 만들어봅시다

 

2. TodoInput 컴포넌트 생성

TodoInput.js를 만들었다

import React from 'react';
import './TodoInput.scss';

const TodoInput = ({value, onChange, onCreate, onKeyPress})=>{
    return(
        <div className="form">
            <input value={value} onChange={onChange} onKeyPress={onKeyPress}/>
            <div className="create-button" onClick={onCreate}>
                추가
            </div>
        </div>
    )
}

export default TodoInput;

value는 인풋 내용

conCreate는 버튼 클릭 시 실행 될 함수

onChange는 인풋 내용이 변경 될 때 실행 될 함수

onKeyPress는 인풋에서 키를 입력 할 때 실행되는 함수. 엔터를 누르면 onCreate를 한 것과 같다

 

    const handleKeyPress=(e)=>{
        if(e.key=='Enter'){
            onInsert();
        }
    }

전에는 이걸 내부에 만들고 바로 onKeyPree={handleKeyPress}하고 불러왔는데 이제는 그냥 위에처럼 써도 되는 것 같다

 

TodoInput.scss 파일을 만들어 준다

.form{
    //border-top: 1px solid #ced4da;
    border-bottom: 2px solid #ffa94d;
    display: flex;
    padding: 1rem;

    input{
        flex:1; //부모 요소에서 add-button을 제외한 나머지 공간 차지
        font-size: 1.1rem;
        outline: none;
        border:none;
        background: transparent;
        border-bottom: 1px solid #ced4da;
        &:focus{
            border-bottom: 1px solid #ffa94d;
        }
    }
}

.create-button{
    width: 5rem;
    height: 2rem;
    margin-left:1rem;
    border: 1px solid #ffa94d;
    color: #ffa94d;
    font-weight: 500;
    font-size: 1.1rem;
    text-align: center;
    line-height: 2rem;
    cursor: pointer;

    &:hover{
        background: #ffa94d;
        color:white;
    }

    &:active{
        background: #fab005;
    }
}

 

App.js에 추가 해 준다

    import React, { Component } from 'react';
    import PageTemplate from './PageTemplate';
    import TodoInput from './TodoInput';

    class App extends Component {
        render() {
            return (
                <PageTemplate>
                    <TodoInput />
                </PageTemplate>
            );
        }
    }

    export default App;

 

이쁜 인풋과 버튼이 나왔다

버튼 위에 커서를 올리면 색이 변한다

 

3. TodoItemList 컴포넌트 생성

일정 정보를 보여주는 TodoItem 컴포넌트를 만들기 전에, 그 컴포넌트 여러 개를 묶어 렌더링하는 TodoItemList 컴포넌트를 생성한다

리스트를 렌더링 할 때는 함수형이 아닌 클래스형 컴포넌트로 작성하는 게 좋다(특히 동적 리스트인 경우)

왜냐하면 클래스형 컴포넌트로 작성해야 나중에 컴포넌트 성능 최적화를 할 수 있기 때문이다

 

TodoItemList.js를 만들어 준다

import React, { Component } from 'react';
import TodoItem from '../TodoItem/TodoItem';

class TodoItemList extends Component {
    render() {
        //const { todos, onToggle, onRemove } = this.props;
        
        return (
            <div>
                <TodoItem text="안녕"/>
                <TodoItem text="리액트 공부"/>
            </div>
        );
    }
}

export default TodoItemList;

todos는 todo 객체들이 들어있는 배열

onToggle은 체크박스를 키고 끄는 함수

onRemove는 아이템을 삭제시키는 함수다

 

지금은 구현하지 않지만 나중을 위해 미리 적어두고 주석처리 해주었다

 

4. TodoItemList 컴포넌트 생성

TodoItem.js 생성

import React, { Component } from 'react';
import './TodoItem.scss';

class TodoItem extends Component {
    render() {
        const { text, checked, id, onToggle, onRemove } = this.props;

        return (
            <div className="todo-item" onClick={() => onToggle(id)}>
                <div className="remove" onClick={(e) => {
                    e.stopPropagation(); //onToggle이 실행되지 않도록
                    onRemove(id)}
                }>&times;</div>
                <div className={`todo-text ${checked && 'checked'}`}>
                    <div>{text}</div>
                </div>
                {
                    checked && (<div className="check-mark">✓</div>)
                }
            </div>
        );
    }
}

export default TodoItem;

text는 todo의 내용

checked는 체크박스 상태

id는 todo의 고유 아이디

onToggle은 체크박스를 키고 끄는 함수

onRemove는 아이템을 삭제시키는 함수

 

        const { text, checked, id, onToggle, onRemove } = this.props;

이렇게 비구조화 할당 문법으로 props안에 있는 값들의 레퍼런스를 만들어 주면,

props를 사용할 때마다 this.props.text 처럼 앞에 this.props를 붙이는 것을 생략할 수 있다

 

앞으로도 class문법으로 컴포넌트를 만들면서 props를 사용할 때 이런 식으로 비구조화 할당을 해 주면 좋다

편리할 뿐만 아니라 렌더링 함수 위쪽에서 이 컴포넌트가 어떤 props를 사용하는지 한 눈에 들어오기 때문이다

 

<div className="todo-item" onClick={() => onToggle(id)}>

최상의 div요소에는 onClick이벤트로 onToggle함수를 연결했다

 

 <div className="remove" onClick={(e) => {
                    e.stopPropagation(); //onToggle이 실행되지 않도록
                    onRemove(id)}
                }>&times;</div>

삭제를 위한 x 마크가 아이템 요소마다 붙을 건데, 거기에 들어갈 onRemove를 만들어주었다

e.stopPropagation()을 하지 않으면, x를 눌렀을 때 onRemove 뿐만 아니라 이 DOM의 부모의 클릭 이벤트에 연결되어 있는 onToggle도 실행이 된다

그러면 onRemove도 눌리고 onToggle도 눌리니까 원하지 않은 동작을 할 수도 있다

이건 onClick을 상위 요소와 하위 요소에 다 사용했을 때의 문제점인데, e.topPropagation을 쓰면 부모를 실행하지 않고 onRemove만 딱 실행해 준다

 

<div className={`todo-text ${checked && 'checked'}`}>

이 코드는 "todo-text"+checked && 'checked' 와 동일하다

checked값에 따라 className이 유동적으로 설정되는 코드다

 

근데 사실 이 부분은 className을 쓰면 훨씬 간단한 코드로 만들 수 있다 (참고: https://velog.io/@velopert/react-component-styling )

참고하고 있는 책 '리액트를 다루는 기술'에서는 classname을 썼는데, 이 글에서는 이 블로그( https://velopert.com/3480 ) 도 함께 참고하느라 이런 방식을 사용했다

저 책을 참고하면 classname을 사용한 todolist 코드를 볼 수 있다

참고로 classname을 사용하면 이렇게 된다

classnames('todo-text', {checked})

 

다음으로 TodoItem.scss를 만들어준다

.todo-item {
  padding: 1rem;
  display: flex;
  align-items: center; //세로 가운데 정렬
  cursor: pointer;
  transition: all 0.15s;
  user-select: none;
}

.todo-item:hover {
  background: #e3fafc;
}

.todo-item:hover .remove {
  opacity: 1;
}

.todo-item + .todo-item {
  border-top: 1px solid #f1f3f5;
}

.remove {
  margin-right: 1rem;
  color: #e64980;
  font-weight: 600;
  opacity: 0;
}

.todo-text {
  flex: 1;
  word-break: break-all;
}

.checked {
  text-decoration: line-through;
  color: #adb5bd;
}

.check-mark {
  font-size: 1.5rem;
  line-height: 1rem;
  margin-left: 1rem;
  color: #3bc9db;
  font-weight: 800;
}

 

App.js에 추가해준다

import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInput from './TodoInput';
import TodoItemList from './TodoItemList';

class App extends Component {
    render() {
        return (
            <PageTemplate>
                <TodoInput />
                <TodoItemList />
            </PageTemplate>
        );
    }
}

export default App;

 

 

이쁘게 UI가 나왔다

아직 추가, 삭제 등의 이벤트는 구현되지 않은 상태다

 

 

 

참고: 리액트를 다루는 기술_김민준 저, https://velopert.com/3480 

 

반응형
Comments