본문 바로가기

react

[리액트를 다루는 기술] 10장 일정 관리 웹 애플리케이션 만들기

1. UI 구성하기 

 

TodoTemplate -화면을 가운데에 정렬시켜 주며, 엡 타이틀(일정 관리)을 보여줌

- children으로 내부 JSX를 props로 받아와서 렌더링해 줌 

TodoInsert - 새로운 항목을 입력하고 추가할 수 있는 컴포넌트 
- state를 통해 인풋의 상태를 관리 
TodoListItem - 각 할일 항목에 대한 정보를 보여 주는 컴포넌트 
- todo 객체를 props로 받아와서 상태에 따라 다른 스타일의 UI를 보여줌
TodoList - todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러개의 TodoListItem 컴포넌트로 변환하여 보여 줌 

 

 

2.1 TodoTemplate 만들기 

 

TodoTemplate.js

import React from 'react';
import styled from 'styled-components';


const TodoTemplateS = styled.div`
    width: 512px;
    // width가 주어진 상태에서 좌우 중앙 정렬
    margin-left: auto;
    margin-right: auto;
    margin-top: 6rem;
    border-radius: 4px;
    overflow: hidden;
`;

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

const Content = styled.div`
    background: white;
`;

const TodoTemplate = ({children}) => {
    return (
        <TodoTemplateS>
            <AppTitle>일정 관리</AppTitle>
            <Content>{children}</Content>
        </TodoTemplateS>
    );
};

export default TodoTemplate;

 

2.2 TodoInsert 만들기 

 

TodoInsert.js

import { FcAcceptDatabase } from "react-icons/fc";
import styled from "styled-components";

const TodoInserts = styled.form`
    display: flex;
    background: #495057;
`;

const Inputs = styled.input`
    background: none;
    outline: none;
    border: none;
    padding: 0.5rem;
    font-size: 1.125rem;
    line-height: 1.5;
    color: white;
    flex: 1;
`;

const Buttons = styled.button`
    background: none;
    outline: none;
    border: none;
    background: #868e96;
    color: white;
    padding-left: 1rem;
    padding-right: 1rem;
    font-size: 1.5rem;
    display: flex;
    align-items: center;
    cursor: pointer;
    transition: 0.1s background ease-in;
`;

const TodoInsert = () => {
    return (
        <TodoInserts>
            <Inputs placeholder="할 일을 입력하세요" />
            <Buttons type="submit">
                <FcAcceptDatabase />
            </Buttons>
        </TodoInserts>
    );
};

export default TodoInsert;

 

2.3 TodoListItem과 TodoList 만들기 

 

TodoListItem.js

import styled from "styled-components";
import { MdOutlineCheckBoxOutlineBlank} from "react-icons/md";
import { CiSquareRemove } from "react-icons/ci";

const TodoListItems = styled.div`
    padding: 1rem;
    display: flex;
    align-items: center; // 세로 중앙 정렬
`;

const Checkbox = styled.div`
    cursor: pointer;
    flex: 1; // 차지할 수 있는 영역 모두 차지
    display: flex;
    align-items: center; // 세로 중앙 정렬
`;

const Text = styled.div`
    margin-left: 0.5rem;
    flex: 1; // 차지할 수 있는 영역 모두 차지
    &.checked {
      svg {
        color: #22b8cf;
      }
      .text {
        color: #adb5bd;
        text-decoration: line-through;
      }
    }
`;

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

const TodoListItem = () => {
    return (
        <>
            <TodoListItems>
                <Checkbox>
                    <MdOutlineCheckBoxOutlineBlank />
                    <Text>할일</Text>
                 </Checkbox> 
            </TodoListItems>
            <Remove>
             <CiSquareRemove color='red' />
            </Remove>
    </>
    )
}

export default TodoListItem;

 

 

TodoList.js

import TodoListItem from "./TodoListItem";
import styled from "styled-components";


const TodoLists = styled.div`
 min-height: 320px;
  max-height: 513px;
  overflow-y: auto;
`;

const TodoList =  () => {
    return (
        <TodoLists>
            <TodoListItem />
            <TodoListItem />
            <TodoListItem />
        </TodoLists>
    )
}

export default TodoList

 

App.js

import TodoTemplate from "./Components/TodoTemplate";
import TodoInsert from "./Components/TodoInsert";
import TodoList from "./Components/TodoList";

const App = () => {
    return (
    <TodoTemplate>
        <TodoInsert />
        <TodoList />

    </TodoTemplate>
    );
};

export default App;

 

 

3. 기능 구현하기 

 

3.1 App에서 todos 상태 사용하기 

 

- 나중에 추가할 일정 항목에 대한 상태는 모두 App 컴포넌트에서 관리 

- App에서 useState를 사용하여 todos라는 상태를 정의하고, todos를 TodoList의 props로 전달

- todos 배열 안에 들어있는 객체는 각 항목의 고유 id, 내용, 완료 여부를 알려주는 값이 포함되어 있음

- 이 배열은 TodoList에 props로 전달

- TodoList에서 이 값을 받아온 후 TodoItem으로 변환하여 렌더링하도록 설정 

 

App.js

import {useState} from 'react';
import TodoTemplate from "./Components/TodoTemplate";
import TodoInsert from "./Components/TodoInsert";
import TodoList from "./Components/TodoList";

const App = () => {
    const [todos, setTodos] = useState([
        {
            id: 1,
            text: '자료구조 과제하기',
            checked: true,
          },
          {
            id: 2,
            text: '캡스톤 과제하기',
            checked: true,
          },
          {
            id: 3,
            text: '일정 관리 앱 만들어 보기',
            checked: false,
          },
        ]);

    return (
    <TodoTemplate>
        <TodoInsert />
        <TodoList todos={todos}/>
    </TodoTemplate>
    );
};

export default App;

 

 

- props로 받아 온 todos 배열을 배열 내장 함수 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링해 줌

- todo 데이터는 통째로 props로 전달 

import TodoListItem from "./TodoListItem";
import styled from "styled-components";


const TodoLists = styled.div`
 min-height: 320px;
  max-height: 513px;
  overflow-y: auto;
`;

const TodoList =  ({todos}) => {
    return (
        <TodoLists>
           {todos.map(todo => (
            <TodoListItem todo={todo} key={todo.id} />
           ))}
        </TodoLists>
    )
}

export default TodoList

 

- TodoListItem 컴포넌트에서 받아 온 todo 값에 따라 UI를 보여줄 수 있도록 수정 

 

TodoListItem.js

import styled from "styled-components";
import { MdOutlineCheckBoxOutlineBlank,MdCheckBox} from "react-icons/md";
import { CiSquareRemove } from "react-icons/ci";

const TodoListItems = styled.div`
    padding: 1rem;
    display: flex;
    align-items: center; // 세로 중앙 정렬

`;

const Checkbox = styled.div`
    cursor: pointer;
    flex: 1; // 차지할 수 있는 영역 모두 차지
    display: flex;
    align-items: center; // 세로 중앙 정렬
    &.checked {
      svg {
        color: #22b8cf;
      }
      .text {
        color: #adb5bd;
        text-decoration: line-through;
      }
    }
`;

const Text = styled.div`
    margin-left: 0.5rem;
    flex: 1; // 차지할 수 있는 영역 모두 차지
`;

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

const Wrapper = styled.div`
  // 엘리먼트 사이사이에 테두리를 넣어줌
  & + & {
    border-top: 1px solid #dee2e6;
  }

`;

const TodoListItem = ({todo}) => {
    const {text, checked} = todo;
    return (
        <Wrapper>
            <TodoListItems>
                <Checkbox>
                    {checked? <MdCheckBox/> :<MdOutlineCheckBoxOutlineBlank /> }
                 </Checkbox> 
                 <Text>{text}</Text>
            </TodoListItems>
            <Remove>
             <CiSquareRemove color='red' />
            </Remove>
    </Wrapper>
    )
}

export default TodoListItem;

 

 

3.2 항목 추가 기능 구현하기 

 

- TodoInsert 컴포넌트에서 인풋 상태를 관리하고 App 컴포넌트에는 todos 배열에 새로운 객체를 추가하는 함수를 만들어 주어야 함 

 

1) TodoInsert value 상태 관리하기 

 

- TodoInsert 컴포넌트에서 인풋에 입력하는 값을 관리할 수 있도록 useState를 사용하여 value라는 상태를 정의 

- useCallback 훅을 사용해 인풋에 넣어 줄 onChange 함수도 작성 

 

TodoInsert.js

...
const TodoInsert = () => {
    const [value, setValue] = useState('');

    const onChange = useCallback(e => {
        setValue(e.target.value);
    },[]);
    return (
        <TodoInserts>
            <Inputs placeholder="할 일을 입력하세요"
            value={value}
            onChange={onChange} />
            <Buttons type="submit">
                <FcAcceptDatabase />
            </Buttons>
        </TodoInserts>
    );
};
...

 

 

2) todos 배열에 새 객체 추가하기 

 

- App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수 만들기 

- 새로운 객체를 만들 때마다 id값에 1씩 더해줘야 함

- id값은 useRef를 사용하여 관리  (id값은 렌더링되는 정보가 아니고, 단순히 새로운 항목을 만들 때 참조되는 값임)

- onInsert 함수는 컴포넌트의 성능을 아낄 수 있도록 useCallback으로 감싸 줌 

- props으로 전달해야 할 함수를 만들 때는 useCallback을 사용하여 함수를 감싸는 것을 습관화 

 

App.js

...
const App = () => {
    // eslint-disable-next-line 
    const [todos, setTodos] = useState([
        {
            id: 1,
            text: '자료구조 과제하기',
            checked: true,
          },
          {
            id: 2,
            text: '캡스톤 과제하기',
            checked: true,
          },
          {
            id: 3,
            text: '일정 관리 앱 만들어 보기',
            checked: false,
          },
        ]);

        //고윳값으로 사용될 id
        //ref를 사용하여 변수 담기 
        const nextId = useRef(4);

        const onInsert = useCallback(
            text => {
                const todo = {
                    id: nextId.current,
                    text,
                    checked:false,
                };
                setTodos(todos.concat(todo));
                nextId.current += 1; //nextId 1씩 더하기 
            },
            [todos],
        )

    return (
    <TodoTemplate>
        <TodoInsert onInsert={onInsert} />
        <TodoList todos={todos}/>
    </TodoTemplate>
    );
};

export default App;

 

 

3) TodoInsert에서 onSubmit 이벤트 설정하기 

 

- 버튼을 클릭하면 발생할 이벤트 설정

- App에서 TodoInsert에 넣어 준 onInsert 함수에 현재 useState를 통해 관리하고 있는 value 값을 파라미터로 넣어서 호출 

- onSubmit 이라는 함수를 만들고, 이를 form의 onSubmit으로 설정 

- 이 함수가 호출되면 props로 받아온 onInsert 함수에 현재 value 값을 파라미터로 넣어서 호출하고, 현재 value 값을 초기화 

- onSubmit 이벤트는 브라우저를 새로고침시킴

  -> 이를 방지하기 위해 e.preventDefault()를 호출

 

TodoInsert.js

...
const TodoInsert = ({onInsert}) => {
    const [value, setValue] = useState('');

    const onChange = useCallback(e => {
        setValue(e.target.value);
    },[]);

    const onSubmit = useCallback(
        e => {
            onInsert(value); 
            setValue('');//value 값 초기화 

            //submit 이벤트는 브라우저에서 새로고침을 발생시킴
            //이를 방지하기 위해 이 함수를 호출
            e.preventDefault();
        },
        [onInsert, value],
    );

        return (
        <TodoInserts onSubmit={onSubmit}>
            <Inputs placeholder="할 일을 입력하세요"
            value={value}
            onChange={onChange} />
            <Buttons type="submit">
                <FcAcceptDatabase />
            </Buttons>
        </TodoInserts>
    );
};

export default TodoInsert;

 

 

3.3 지우기 기능 구현하기 

 

- 리액트 컴포넌트에서 배열의 불변성을 지키면서 배열 원소를 제거해야 할 경우, 배열 내장 함수인 filter를 사용하면 매우 간편함 

 

1) 배열 내장 함수 filter

 

 - 기존의 배열은 그대로 둔 상태에서 특정 조건을 만족하는 원소들만 따로 추출하여 새로운 배열을 만들어 줌 

const array = [1,2,3,4,5,6,7,8,9,10];
const biggerThanFive = array.filter(number=>number>5);
//결과: [6,7,8,9,10]

 

 

2) todos 배열에서 id로 항목 지우기 

 

- App 컴포넌트에 id를 파라미터로 받아와서 같은 id를 가진 항목을 tods 배열에서 지우는 함수 

- 함수를 만들고 나서 TodoList의 props로 설정 

 

App.js

...
const onRemove = useCallback(
            id => {
                setTodos(todos.filter(todo=>todo.id!==id));
            },
            [todos],
        );

    return (
    <TodoTemplate>
        <TodoInsert onInsert={onInsert} />
        <TodoList todos={todos} onRemove={onRemove}/>
    </TodoTemplate>
    );
};

 

 

2) TodoListItem에서 삭제 함수 호출하기 

 

- TodoListItem에서 onRemove 함수를 사용하려면 TodoList 컴포넌트를 거쳐야 함 

- props로 받아 온 onRemove 함수를 TodoListItem에 그대로 전달 

 

TodoList.js

const TodoList =  ({todos, onRemove}) => {
    return (
        <TodoLists>
           {todos.map(todo => (
            <TodoListItem todo={todo} key={todo.id} onRemove={onRemove} />
           ))}
        </TodoLists>
    )
}

 

- 삭제 버튼을 누르면 TodoListItem에서 onRemove 함수에 현재 자신이 가진 id를 넣어서 삭제 함수를 호출하도록 설정

 

TodoListItem.js

...
const TodoListItem = ({todo, onRemove}) => {
    const {id, text, checked} = todo;
    return (
        <Wrapper>
            <TodoListItems>
                <Checkbox>
                    {checked? <MdCheckBox/> :<MdOutlineCheckBoxOutlineBlank /> }
                 </Checkbox> 
                 <Text>{text}</Text>
            </TodoListItems>
            <Remove onClick={()=>onRemove(id)} >
             <CiSquareRemove color='red' />
            </Remove>
    </Wrapper>
    )
}

export default TodoListItem;

 

 

3.4 수정 기능 

 

- onToggle이라는 함수를 App 에 만들고 해당 함수를 TodoList 컴포넌트에 props로 넣어줌

- 그 다음에는 TodoList를 통해 TodoListItem까지 전달해 주면 됨 

 

1) onToggle 구현하기 

 

- 배열 내장 함수 map을 사용하여 특정 id를 가지고 있는 객체의 checked 값을 반전 시켜줌 

- 불변성을 유지하면서 특정 배열 원소를 업데이트해야 할 때 이렇게 map을 사용하면 짧은 코드로 쉽게 작성 가능 

- todo.id와 현재 파라미터로 사용된 id값이 같을 떄는 새로운 객체를 생성하지만, id 값이 다를 때는 변화를 주지 않고 처음 받아왔던 상태 그대로 반환 

- map을 사용하여 만든 배열에서 변화가 필요한 원소만 업데이트되고 나머지는 그대로 남아 있게됨 

 

App.js

...
const onToggle = useCallback(
            id=> {
                setTodos(
                    todos.map(todo=>
                        todo.id === id ? {...todo, checked: !todo.checked} : todo,),
                );
            },
            [todos],
        );

    return (
    <TodoTemplate>
        <TodoInsert onInsert={onInsert} />
        <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
    </TodoTemplate>
    );
};

 

 

2) TodoListItem에서 토글 함수 호출하기 

 

TodoList.js

const TodoList =  ({todos, onRemove, onToggle}) => {
    return (
        <TodoLists>
           {todos.map(todo => (
            <TodoListItem todo={todo} key={todo.id} onRemove={onRemove} onToogle={onToggle} />
           ))}
        </TodoLists>
    )
}

 

TodoListItem.js

const TodoListItem = ({todo, onRemove, onToggle}) => {
    const {id, text, checked} = todo;
    return (
        <Wrapper>
            <TodoListItems>
                <Checkbox  onClick={()=>onToggle(id)}>
                    {checked? <MdCheckBox/> :<MdOutlineCheckBoxOutlineBlank /> }
                 </Checkbox> 
                 <Text>{text}</Text>
            </TodoListItems>
            <Remove onClick={()=>onRemove(id)} >
             <CiSquareRemove color='red' />
            </Remove>
    </Wrapper>
    )
}

export default TodoListItem;