JS_리액트_TodoList
프로젝트/소규모프로젝트들

JS_리액트_TodoList

728x90

리액트를 사용해서 TodoList를 만드는 소규모 프로젝트를 진행해보겠다. 패스트캠퍼스의 강의를 참조하였습니다!

 

리액트 프로젝트를 하나 만든 후에 2개의 라이브러리를 설치할 것이다.

 

yarn add styled-components react-icons

 

 

여러 컴포넌트를 만들고 관리할 것인데 간단한 역할을 먼저 알아보자.

 

 

TodoTemplate

 

Todolist의 레이아웃을 담당하고 하얀색 테두리를 담당한다.

 

TodoHead

날짜시간, 할일의 개수를 가진다

TodoList

할일에 대한 정보를 map내장함수를 사용하여 랜더링할것이다.

 

TodoItem

할일을 표시한다.

 

TodoCreate

새로운 할일을 등록할 수 있게 한다.

 

 

 

1. 회색배경화면 만들기

//App.js
import React from 'react'
import {createGlobalStyle} from 'styled-components';

const GlobalStyle = createGlobalStyle`
  body {
    background : #e9ecef;
  }
`;

function App() {
  return (
    <>
      <GlobalStyle />
        <div>hi</div>      
    </>
    
  );
}

export default App;

 

먼저 회색 배경화면을 GlobalStyle을 이용해서 만들어주었다.

 

 

2. 하얀색 박스 만들기 (TodoTemplate)

 

//TodoTemplate.js
import React from 'react';
import styled from 'styled-components';

const TodoTemplateBlock = styled.div`
    width : 512px;
    height : 768px;

    position : relative;
    background : white;
    border-radius : 16px;
    box-shadow : 0 0 8px rgba(0,0,0,0.04);

    margin : 0 auto;
    margin-top : 96px;
    margin-bottom : 32px;

    display : flex;
    flex-direction : column;
`;


function TodoTemplate({children}) {
    return (
        <TodoTemplateBlock>{children}</TodoTemplateBlock>
    );
    
}


export default TodoTemplate;

components파일 안에 TodoTemplate를 만들어서 App.js와 연동시켜주었다.

 

3. TodoHead

 

//TodoHead.js
import React from 'react';
import styled from 'styled-components';

const TodoHeadBlock = styled.div`
    padding-top : 48px;
    padding-left : 32px;
    padding-right : 32px;
    padding-bottom : 24px;
    border-bottom : 1px solid #e9ecef;

    h1 {
        margin : 0;
        font-size : 36px;
        color : #343a40;
    }

    .day {
        margin-top : 4px;
        color : #868e96;
        font-size : 21px;
    }

    .tasks-left {
        color : #20c997;
        font-size : 18px;
        margin-top :40px;
        font-weight: bold;
    }
`;

function TodoHead() {
    return (
        <TodoHeadBlock>
            <h1>2021년 12월 27일</h1>
            <div className='day'>월요일</div>
            <div className='task-left'>할일 2개 남음</div>
        </TodoHeadBlock>
    );
    
}


export default TodoHead;

마찬가지로 위와같이 설정을 해두면 된다. 우선 글자로 처리해주었고 기능을 추가할때 변수들로 추가하여 동작하게 만들어 보겠다.

 

 

function App() {
  return (
    <>
      <GlobalStyle />
        <TodoTemplate>
          <TodoHead />
        </TodoTemplate>      
    </>
    
  );
}

4. TodoList

 

List가 나올 공간을 확인해보자.

 

//TodoList.js
import React from 'react';
import styled from 'styled-components';


const TodoListBlock = styled.div`
    flex : 1; /* 자신이 차지할 수 있는 모든 영역을 차지 */

    padding : 20px 32px;
    padding-bottom : 48px;
    overflow-y : auto;
    background : gray;
`;

function TodoList() {
    return (
        <TodoListBlock>TodoList</TodoListBlock>
    );
    
}


export default TodoList;

 

회색으로 공간처리가 잘된것을 볼 수 있다. 이제 다시

 

 

background : auto

 

로 바꾸어서 회색이 안보이게 설정하면 되겠다.

 

 

5. TodoItem

 

이제 내용물을 작성해보자. 

 

//TodoItem.js
import React from 'react';
import styled,{css} from 'styled-components';
import { MdDone, MdDelete } from 'react-icons/md' //react-icon에서 가져옴


const Remove = styled.div` 
    opacity : 0;
    display : flex;
    align-items : center;
    justify-content : center;
    color : #dee2e6;
    font-size : 24px;
    cursor : pointer;
    &:hover {
        color : #ff6b6b;
    }
`;  //쓰레기통 아이콘 표시

const CheckCircle = styled.div`
    width : 32px;
    height : 32px;
    border-radius : 16px;
    border : 1px solid #ced4da;
    font-size : 24px;
    display : flex;
    align-items: center;
    justify-content : center;
    margin-right : 20px;
    cursor : pointer;

    ${props => props.done && css`
        border : 1px solid #38d9a9;
        color : #38d9a9;
    `}
`; //체크 아이콘 표시

const Text = styled.div `
    flex : 1;
    font-size : 21px;
    color : #495057;
    ${props => props.done && css`
        color : #ced4da;
    `}

`;  //Text표시

const TodoItemBlock = styled.div`
    
    display : flex;
    align-items : center;
    padding-top : 12px;
    padding-bottom : 12px;

    &:hover {
        ${Remove} {   /* remove컴포넌트에서 만든 classname*/ 
            opacity : 1; /* 커서를 올렸을때 1 */
        }
    }
`;  //위 3개의 내용을 포함



function TodoItem( {id,done,text}) {
    return (
        <TodoItemBlock>
            <CheckCircle done={done} >{done && <MdDone/>}</CheckCircle>
            <Text done={done}>{text}</Text>
            <Remove>
                <MdDelete/>
            </Remove>
        </TodoItemBlock>
    );
    
}


export default TodoItem;

Item은 조금더 복잡한데, 총 4개의 컴포넌트를 스타일링해주었고 살펴볼 것으로는 TodoitemBlock에서 ${Remove}를 가져와서 사용함으로써 커서를 올렸을때 쓰레기통이 보여지게끔 만들수 있다는 것이다.

 

icon은 React-icons 모듈에서 가져왔다. 

 

https://supersfel.tistory.com/131?category=1063079 

 

17_리액트_CSS Module

className이 겹치지 않게 작성하는 팁 1. 컴포넌트 이름을 고유하게 지정 2. 최상위 엘리먼트의 클래스이름을 컴포넌트 이름과 똑같게 (대문자를 사용) 3. 그 내부에서 셀렉터 사용 CSS Module CSS Module

supersfel.tistory.com

이전에 적은 17번째 글에서 내용을확인할 수 있다.

 

6.TodoCreate

 

더보기
//TodoCreate.js
import React , {useState} from 'react';
import styled, {css} from 'styled-components';
import {MdAdd} from 'react-icons/md';

const CircleButton = styled.button `
    background : #38d9a9;
    &:hover {
        background : #63e6be;
    }
    &:active {
        background : #20c997;
    }

    z-index : 5;   /*내용을 가리기 위함*/
    cursor : pointer;
    width : 80px;
    height : 80px;
    display : flex;
    align-items : center;
    justify-content : center;

    position : absolute;
    left : 50%;
    bottom : 0px;
    transform : translate(-50%,50%);  /* 정확한 버튼위치 찾기를 위함*/ 

    font-size : 60px;
    color : white;
    border-radius : 40px;

    border : none;
    outline : none;


    transition : 0.125s all ease-in;
    ${props => props.open && css`     /* 눌렀을때 x로 보이게끔 */
        background : #ff6b6b;
        &:hover {
            background : #ff8787;
        }
        &:active {
            background : #fa5252;
        }

        transform : translate(-50%,50%) rotate(45deg);
    `}

`;

const InsertFormPositioner = styled.div`

        width : 100%;
        bottom : 0;
        left : 0;
        position : absolute;
`;

const InsertForm = styled.div`
    background : #f8f9fa;
    padding : 32px;
    padding-bottom : 72px;
    border-bottom-left-radius : 16px; /* 둥근 모서리가 삐져나오지 않게*/
    border-bottom-right-radius : 16px;
    border-top : 1px solid #e9ecef;
`;


const Input = styled.input`
    
    padding : 12px;
    border-radius : 4px;
    border : 1px solid #dee2e6;
    width : 100%;
    outline : none;
    font-size : 18px;

    box-sizing : border-box;
`;


function TodoCreate() {

    const [open,setOpen] = useState(false);
    const onToggle = () => setOpen(!open);




    return (
        <>        
            {open && (
                < InsertFormPositioner > 
                    < InsertForm >
                        <Input placeholder='할 일을 입력후, Enter를 누르세요' autoFocus />
                    </InsertForm>
                </InsertFormPositioner >
            )}
            <CircleButton onClick ={onToggle} open={open}>
                < MdAdd />
            </CircleButton>
        </>

    );
    
}


export default TodoCreate;

조금 길어서 접은글 안에 넣어두었다. 

 

실제 체크 박스를 가져오고, 눌렀을때 MdAdd 버튼을 돌리고 색을 변경한다.

 

코드가 길어서 헷갈릴 순 있으나 대부분 시각효과 설정이고 useState를 통해 open값으로 열렸을때와 닫혔을때의 값을 관리하고, 열렸을때 Input태그가 있는 Form을 열어준다는게 핵심 기능이다.

 

 

Context API활용

 

Context API를 활용하지 않고 Props만 가지고 컴포넌트를 넘겨준다면

 

 

 

위와 같은 상태가 된다. 모든 정보가 App에 정보가 있는 것이다.

 

Context API를 활용하게되면

 

위처럼 따로 Props를 넘겨주지 않아도 괜찮다.

 

상태 업데이트만 관리해주면 되게 되는 것이다.

 

그렇다고 Context API를 무조건 쓰는게 맞지는 않다. 프로젝트의 규모가 엄청 크지않으면 굳이 필요없지만 규모가클 수록 필요하다.

 

 

7. reducer 만들기

 

 

상태 관리를 위해 Reducer을 사용하여 만들 것이다. 

 

 

더보기
//TodoContext.js
import React,{useReducer, createContext, useContext, useRef} from 'react';
import TodoList from './TodoList';

const initialTodos = [
    {
        id : 1,
        text : '민규의 첫번째 할일',
        done : true,
    },
    {
        id : 2,
        text : '민규의 두번째 할일',
        done : true,
    },
    {
        id : 3,
        text : '민규의 세번째 할일',
        done : false,
    },
    {
        id : 4,
        text : '민규의 네번째 할일',
        done : false,
    },
];



function todoReducer(state,action) {
    switch (action.type){
        case 'CREATE' :
            return state.concat(action.todo);
        case 'TOGGLE' :
            return state.map(
                todo => todo.id === action.id ? { ...todo,done : !todo.done } : todo
            );
        case 'REMOVE' :
            return state.filter(todo => TodoList.id !== action.id);
    
    default :
        throw new Error(`Unhandled action type : ${action.type}`);
    }
}

const TodoStateContext = createContext();
const TodoDispatchContext = createContext();
const TodoNextIdContext = createContext(); //id값 관리





export function TodoProvider({children}) {
    const [state,dispatch] = useReducer(todoReducer, initialTodos);
    const nextId = useRef(5);


    return (
        <TodoStateContext.Provider value = {state}>
            <TodoDispatchContext.Provider value = {dispatch}>
                <TodoNextIdContext.Provider value = {nextId}>
                    {children}
                </TodoNextIdContext.Provider>                
            </TodoDispatchContext.Provider>
        </TodoStateContext.Provider>
    );    
}


export function useTodoState() {    //나중에 Todolist에서 그냥 const state = useTodoState();로 선언해서 사용이 가능하도록
    const context = useContext(TodoStateContext);
    if (!context) {
        throw new Error('Cannot find TodoProvider');
    }
    return context;
}

export function useTodoDispatch() { //랜더링할 필요가 없을때 불필요한 랜더링을 막기위해서 나누어서 만듬
    
    const context = useContext(TodoDispatchContext);
    if (!context) {
        throw new Error('Cannot find TodoProvider');
    }
    return context;
}

export function useTodoNextId() {
    const context = useContext(TodoNextIdContext);
    if (!context) {
        throw new Error('Cannot find TodoProvider');
    }
    return context;
}

동작방식은

https://supersfel.tistory.com/127?category=1063079 

 

13_리액트_Context API,immer

Context API 이 전글의 예제에서도 그렇지만 리액트에서 여러개의 컴포넌트를 거쳐가면서 Props를 전달해줘야 하는 경우가 많다. 즉, A -> B -> C -> D 와같이 차근차근 Props를 넘겨주는 경우인데, 이를 A

supersfel.tistory.com

위글을 다시한번 보자

 

 

TodoList에서 state를 찍어보면

function TodoList() {

    const state = useTodoState();
    console.log(state)

    return (
        <TodoListBlock>
            <TodoItem text ="프로젝트 생성하기" done = {true}></TodoItem>
            <TodoItem text ="민규" done = {true}></TodoItem>
            <TodoItem text ="정민규" done = {false}></TodoItem>
            <TodoItem text ="나는 정민규" done = {true}></TodoItem>
        </TodoListBlock>
    );
    
}

잘 나오고 있음을 확인할 수 있다.

 

 

 

8. 기능 구현하기

 

8-1 ) Todohead

 

function TodoHead() {
    const todos = useTodoState();
    const undoneTasks = todos.filter(todo => !todo.done);   //할일 개수파악

    const today = new Date();
    const dateString = today.toLocaleDateString('ko-KR', {
        year : 'numeric',
        month : 'long',
        day : 'numeric'
    });
    const dayName = today.toLocaleDateString('ko-KR', {
        weekday : 'long'
    });

    return (
        <TodoHeadBlock>
            <h1>{dateString}</h1>
            <div className='day'>{dayName}</div>
            <div className='task-left'>할일 {undoneTasks.length}개 남음</div>
        </TodoHeadBlock>
    );
    
}


export default TodoHead;

8-2 ) TodoList

 

 

 

        <TodoListBlock>
            <TodoItem text ="프로젝트 생성하기" done = {true}></TodoItem>
            <TodoItem text ="민규" done = {false}></TodoItem>
            <TodoItem text ="정민규" done = {true}></TodoItem>
            <TodoItem text ="나는 정민규" done = {true}></TodoItem>
        </TodoListBlock>

위 같았던 코드를 아래처럼 바꾸어주면 된다.

 

<TodoListBlock>
            {todos.map (
                todo => <TodoItem
                key = {todo.id}
                id={todo.id}
                text={todo.text}
                done = {todo.done}
            />
            )}
</TodoListBlock>

 

8-3 TodoCreate

 

 

function TodoCreate() {

    const [open,setOpen] = useState(false);
    const [value,setValue] = useState(''); //input값 관리

    const onToggle = () => setOpen(!open);
    const onChange = (e) => setValue(e.target.value);
    const onSubmit = e => {
        e.preventDefault(); //입력후 엔터키를 눌러도 새로고침이 안된다
        dispatch({
            type : 'CREATE',
            todo : {
                id : nextId.current,
                text : value,
                done : false,
            }
        });
        setValue('');
        setOpen(false);
        nextId.current +=1;
    }

    const dispatch = useTodoDispatch();
    const nextId = useTodoNextId();


    return (
        <>        
            {open && (
                < InsertFormPositioner > 
                    < InsertForm onSubmit={onSubmit}>
                        <Input 
                            placeholder='할 일을 입력후, Enter를 누르세요' 
                            autoFocus 
                            onChange={onChange}
                            value={value}
                        />
                    </InsertForm>
                </InsertFormPositioner >
            )}
            <CircleButton onClick ={onToggle} open={open}>
                < MdAdd />
            </CircleButton>
        </>

    );
    
}

 

그러면 입력 기능이 추가되게 된다.

728x90