본문 바로가기
과제

항해99 React 2주차 과제

by dev정리 2022. 10. 8.

1주차에 만들었던 프로젝트를 react-router-dom, styled-components, redux를 사용해서 

My Todo List 를 다시 만드는게 과제다...

 

저번 프로젝트에 새로 배운것들을 추가만 할까 생각 했지만 복습도 할겸 다시 만들기로 결정했다...

 

 

 

npx create-react-app todo-list-redux
yarn add styled-components
yarn add redux react-redux
yarn add react-router-dom

우선 프로젝트를 만들고 필요한 라이브러리를 다운받았다.

 

 

그리고 폴더와 파일들을 만들었다...

styled-components를 사용할거라 css는 뺐다 개꿀

 

 

서버를 돌려봤다 잘 되는군...

이제 리액트 로고가 돌아가는걸 보며 뭐 부터 할지 생각한다....

 

Router를 사용하니까 페이지 이동부터 만들고

html css꾸미고

컴포넌트화 하면서

기능을 넣으면 되겠다... 후 말로는 쉽군 

 

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
//페이지들..
import TodoList from "../pages/TodoList";
import Detail from "../pages/Detail";

const Router = () => {
    return (
        <BrowserRouter>
            <Routes>
                {/* 
						Routes안에 이렇게 작성합니다. 
						Route에는 react-router-dom에서 지원하는 props들이 있습니다.

						paht는 우리가 흔히 말하는 사용하고싶은 "주소"를 넣어주면 됩니다.
						element는 해당 주소로 이동했을 때 보여주고자 하는 컴포넌트를 넣어줍니다.
				 */}
                <Route path="/" element={<TodoList />} />
                <Route path="/detail" element={<Detail />} />
            </Routes>
        </BrowserRouter>
    );
};

export default Router;

우선 라우터 페이지를 작성 했다.

그리고 App에서 router를 불러왔다.

import './App.css';
import Router from './shared/Router';

function App() {
  return (
    <Router />
  );
}

export default App;

페이지 이동이 잘 된다 ㅎㅎ router 끝났쥬?

컴포넌트 화하며 꾸미기 전에 store를 만들고 reducer를 먼저 만들어야겠다...

생각해 보니 이걸 먼저 만들어야 겠다...

 

configStore.js

import { createStore } from "redux";
import { combineReducers } from "redux";

// 리듀서
import todoList from "../modules/todoList";

const rootReducer = combineReducers({
    todoList: todoList,
});

//스토어
const store = createStore(rootReducer);

//스토어 익스포트
export default store;

우선 store를 선언을 했다.

 

 

todoList.js

//action 타입
const ADD_TODO = 'ADD_TODO';

//action 함수
export const addTodo = (payload) => {
    if (payload.todo !== undefined && payload.todo !== "") {
        return {
            type: ADD_TODO,
            id: payload.length,
            title: payload.title,
            content: payload.todo,
            done: payload.done
        };
    }
}

// 초기 상태값
const initTodoList = [
    {
        id: 0,
        title: `리액트 공부하기`,
        content: `리액트 기초를 공부해봅시다.`,
        done: false,
    },
    {
        id: 1,
        title: `리액트 공부하기2`,
        content: `리액트 기초를 공부해봅시다2.`,
        done: true,
    }
];
// 리듀서
const todoList = (state = initTodoList, action) => {
    switch (action.type) {
        case ADD_TODO:
            return [
                ...state,
                {
                    id: action.id,
                    title: action.title,
                    content: action.content,
                    done: action.done
                }
            ];
        default:
            return state;
    }
};

export default todoList;

우선 추가하는 기능만 만들었다.. 잘 되면 업데이트랑 삭제도..

 

import React from 'react';
//스토어의 상태값과 action을 취하기 위해
import { useSelector, useDispatch } from 'react-redux';
//리듀서의 액션 함수 쓰기위해
import { addTodo } from '../redux/modules/todoList';

const TodoList = () => {
    const gTodoList = useSelector(state => state.todoList);
    console.log(gTodoList);

    return (
        <div>
            TodoList
        </div>
    );
};

export default TodoList;

todoList 페이지에 스토어를 연결하고 실행 해봤다

 

깔끔하게 에러 짜릿하다.

 

에러 복사후 번역해보니 스토어 범위를 안정해줬다...

뭐야? 왜 친절해?

구글링 전에 번역해보면 바로 처리할수 있는것들이 대부분일때가 많다

 

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

//스토어 가져오기
import store from "./redux/config/configStore";
//스토어 사용범위를 정할 프로바이더 가져오기
import { Provider } from "react-redux";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  // 프로바이더 선언및 속성으로 스토어주기
  <Provider store={store}>
    <App />
  </Provider>
);

reportWebVitals();

Provider로 스토어의 범위를 정해줬다.

 

바로 잘 나온다.

역시 하나씩 확인하면서 해야해...

 

이제 컴포넌트들을 페이지 뿌려줘야겠다.

가즈아 폭풍 복붙!

 

import React from 'react';

import Layout from "../components/layout/Layout";
import List from "../components/list/List";
import Form from "../components/form/Form";
import Header from "../components/header/Header";

const TodoList = () => {
    return (
        <Layout>
            <Header />
            <Form />
            <List />
        </Layout>
    );
};

export default TodoList;

생각해 보니 state는 List컴포넌트에서 불러와야겠다...

redux를 쓰니 컴포넌트에 속성으로 useSate를 넘길 필요가 없어 편하다.

일단 뭐가 보여야 하니 Layout과 Header에 내용을 채웠다.

 

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

const StLayoutDiv = styled.div`
    margin: 0 auto;
    max-width: 1200px;
    min-width: 800px;
`;

function Layout(props) {
    return (
        <StLayoutDiv>
            {props.children}
        </StLayoutDiv>
    );
}

export default Layout;
import React from "react";
import styled from "styled-components";

const StHeaderDiv = styled.div`
    align-items: center;
    border: 1px solid #ddd;
    display: flex;
    height: 50px;
    justify-content: space-between;
    padding: 0 20px;
`

function Header() {
    return (
        <StHeaderDiv>
            <div>My Todo List</div>
            <div>React</div>
        </StHeaderDiv>
    )
}

export default Header;

실행

이런식으로 html과 css부터 채워야겠다.

나는 멍청해서 보면서 해야한다 ㅠ

 

 

일단 기능은없고 css component를 이용해서 만들었다.

소스는 길어서 생략한다.

이제 기능을 넣으면 끝이다..

 

import React from "react";
import Todo from "../todo/Todo";
import styled from "styled-components";

//state
import { useSelector } from 'react-redux';

const StListContainerDiv = styled.div`
    padding: 0 24px;
`
const StListWrapperDiv = styled.div`
     display: flex;
    flex-wrap: wrap;
    gap: 12px;
`
const StH2 = styled.h2`
    display: block;
    font-size: 1.5em;
    margin-block-start: 0.83em;
    margin-block-end: 0.83em;
    margin-inline-start: 0px;
    margin-inline-end: 0px;
    font-weight: bold;
`

function List() {
    const gTodoList = useSelector(state => state.todoList);
    //console.log(gTodoList)
    return (
        <StListContainerDiv>
            <StH2>Working.. 🔥</StH2>
            <StListWrapperDiv>
                {gTodoList.map((val) => {
                    if (val.done === false) return (<Todo obj={val} key={val.id} />);
                })}
            </StListWrapperDiv>
            <StH2>Done..! 🎉</StH2>
            <StListWrapperDiv>
                {gTodoList.map((val) => {
                    if (val.done) return (<Todo obj={val} key={val.id} />);
                })}
            </StListWrapperDiv>
        </StListContainerDiv>
    )
}

export default List;

list에서 store의 state를 받아와 뿌려준다.

 

 

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

const StTodoContainerDiv = styled.div`
    border: 4px solid teal;
    border-radius: 12px;
    padding: 12px 24px 24px;
    width: 270px;
`
const StButtonWrap = styled.div`
    display: flex;
    gap: 10px;
    margin-top: 24px;
`
const StTodoButton = styled.button`
    border: 1px solid ${(props) => props.buttonColor};
    height: 40px;
    width: 120px;
    background-color: rgb(255, 255, 255);
    border-radius: 12px;
    cursor: pointer;
`
const StDetail = styled.a`
    text-decoration: none;
`

function Todo({ obj }) {
    return (
        <StTodoContainerDiv>
            <div>
                <StDetail href="/detail">상세보기</StDetail>
                <h2>{obj.title}</h2>
                <div>{obj.content}</div>
            </div>
            <StButtonWrap>
                <StTodoButton
                    buttonColor="red"
                    onClick={() => {
                        // deleteTodo(obj.id);
                    }}
                >
                    삭제하기
                </StTodoButton>
                <StTodoButton
                    buttonColor="green"
                    onClick={() => {
                        // changeDone(obj.id, obj.done);
                    }}
                >
                    {obj.done ? "취소" : "완료"}
                </StTodoButton>
            </StButtonWrap>
        </StTodoContainerDiv>
    );
}

export default Todo;

todo페이지 기능은 아직이다.

 

 

state의 초기값으로 잘 뿌려진다.

이제 추가하기 기능을 만들어 보자

 

import React, { useState } from "react";
import styled from "styled-components";

//스토어의 상태값과 action을 취하기 위해
import { useSelector, useDispatch } from 'react-redux';
//리듀서의 액션 함수 쓰기위해
import { addTodo } from '../../redux/modules/todoList';

const StAddFormDiv = styled.div`
    background-color: rgb(238, 238, 238);
    border-radius: 12px;
    margin: 0px auto;
    display: flex;
    -webkit-box-align: center;
    align-items: center;
    -webkit-box-pack: justify;
    justify-content: space-between;
    padding: 30px;
    gap: 20px;
`
const StInputGroupDiv = styled.div`
    align-items: center;
    display: flex;
    gap: 20px;
`
const StFormLabel = styled.label`
    font-size: 16px;
    font-weight: 700;
`
const StAddInput = styled.input`
    border: none;
    border-radius: 12px;
    height: 40px;
    padding: 0 12px;
    width: 240px;
`
const StAddButton = styled.button`
    background-color: teal;
  border: none;
  border-radius: 10px;
  color: #fff;
  font-weight: 700;
  height: 40px;
  width: 140px;
`

function Form() {

    const [inputText, setInputText] = useState({ title: "", content: "" });
    //store의 상태값 변수로 선언
    const gTodoList = useSelector(state => state.todoList);
    //store의 상태를 변경할 dispatch선언
    const dispatch = useDispatch();

    //인풋 바뀔때 마다 값넣기
    const getInput = (e) => {
        setInputText({
            ...inputText,
            [e.target.name]: e.target.value,
        });
    };

    //id최대값
    const getMaxId = () => {
        let stateIdArr = gTodoList.map(element => {
            return Number(element.id)
        });
        return Math.max(...stateIdArr);
    }

    return (
        <StAddFormDiv>
            <StInputGroupDiv>
                <StFormLabel>제목</StFormLabel>
                <StAddInput
                    type="text"
                    name="title"
                    onChange={getInput}
                    value={inputText.title || ""}
                />
                <StFormLabel>내용</StFormLabel>
                <StAddInput
                    type="text"
                    name="content"
                    onChange={getInput}
                    value={inputText.content || ""}
                />
            </StInputGroupDiv>
            <StAddButton onClick={() => {
                if (inputText.title === "" || inputText.content === "" || inputText.title === undefined || inputText.content === undefined) {
                    alert("빈칸좀..")
                } else {
                    let obj = {
                        id: Number(getMaxId()) + 1,
                        title: inputText.title,
                        content: inputText.content
                    }
                    dispatch(addTodo(obj));
                    setInputText({});
                }
            }}>
                추가하기
            </StAddButton>
        </StAddFormDiv>
    );
}

export default Form;

state의 id의 최대값을 찾아 +1해준 값을 id에 넣어 추가해줬다.

length로 하려했는데 하지말라고해서...

length로 했을때 id가 0,1,2가있다고 치면

1을 지우고 새로 만들면 id가 2인것이 2개가 될것이다.

 

이제 완료 취소 삭제하기 상세보기만 하면 끝이다.

 

 

//action 타입
const ADD_TODO = 'ADD_TODO';
const CHANGE_DONE = 'CHANGE_DONE';
const DELETE_TODO = 'DELETE_TODO';

//action 함수
export const addTodo = (obj) => {
    return {
        type: ADD_TODO,
        id: obj.id,
        title: obj.title,
        content: obj.content,
        done: false
    };
}

export const changeDone = (id) => {
    return {
        type: CHANGE_DONE,
        id: id
    }
}

export const deleteTodo = (id) => {
    return {
        type: DELETE_TODO,
        id: id
    }
}

// 초기 상태값
const initTodoList = [
    {
        id: 0,
        title: `리액트 공부하기`,
        content: `리액트 기초를 공부해봅시다.`,
        done: false,
    },
    {
        id: 1,
        title: `리액트 공부하기2`,
        content: `리액트 기초를 공부해봅시다2.`,
        done: true,
    }
];
// 리듀서
const todoList = (state = initTodoList, action) => {
    switch (action.type) {
        case ADD_TODO:
            return [
                ...state,
                {
                    id: action.id,
                    title: action.title,
                    content: action.content,
                    done: action.done
                }
            ];
        case CHANGE_DONE:
            state.map((val, i, arr) => {
                if (val.id === action.id) {
                    arr[i].done ? arr[i].done = false : arr[i].done = true
                }
            })
            return [...state];
        case DELETE_TODO:
            state.map((val, i, arr) => {
                if (val.id === action.id) {
                    arr.splice(i, 1);
                }
            })
            return [...state];
        default:
            return state;
    }
};

export default todoList;

이전에 만들었던 기능을 리듀서에 그대로 복붙했다.

 

 

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

//action을 취하기 위해
import { useDispatch } from 'react-redux';
//리듀서의 액션 함수 쓰기위해
import { deleteTodo, changeDone } from '../../redux/modules/todoList';

const StTodoContainerDiv = styled.div`
    border: 4px solid teal;
    border-radius: 12px;
    padding: 12px 24px 24px;
    width: 270px;
`
const StButtonWrap = styled.div`
    display: flex;
    gap: 10px;
    margin-top: 24px;
`
const StTodoButton = styled.button`
    border: 1px solid ${(props) => props.buttonColor};
    height: 40px;
    width: 120px;
    background-color: rgb(255, 255, 255);
    border-radius: 12px;
    cursor: pointer;
`
const StDetail = styled.a`
    text-decoration: none;
`

function Todo({ obj }) {
    const dispatch = useDispatch();
    return (
        <StTodoContainerDiv>
            <div>
                <StDetail href={'/detail/' + obj.id}>상세보기</StDetail>
                <h2>{obj.title}</h2>
                <div>{obj.content}</div>
            </div>
            <StButtonWrap>
                <StTodoButton
                    buttonColor="red"
                    onClick={() => {
                        dispatch(deleteTodo(obj.id));
                    }}
                >
                    삭제하기
                </StTodoButton>
                <StTodoButton
                    buttonColor="green"
                    onClick={() => {
                        dispatch(changeDone(obj.id));
                    }}
                >
                    {obj.done ? "취소" : "완료"}
                </StTodoButton>
            </StButtonWrap>
        </StTodoContainerDiv>
    );
}

export default Todo;

todo컴포넌트에서 기능을 구현했다.

redux개꿀...

 

case GET_DETAIL:
            const selTodo = state.filter((val) => {
                return (val.id === action.id);
            })
            return selTodo;

상세페이지 리듀서 추가

 

 

import React, { useEffect } from 'react';
import styled from 'styled-components';
import { useSelector, useDispatch } from 'react-redux';
import { useParams, Link } from 'react-router-dom'
import { getDetail } from '../redux/modules/todoList';

const StDetailWrapDiv = styled.div`
        border: 2px solid rgb(238, 238, 238);
    width: 100%;
    height: 100vh;
    display: flex;
    -webkit-box-align: center;
    align-items: center;
    -webkit-box-pack: center;
    justify-content: center;
`
const StDetailBorderDiv = styled.div`
        width: 600px;
    height: 400px;
    border: 1px solid rgb(238, 238, 238);
    display: flex;
    flex-direction: column;
    -webkit-box-pack: justify;
    justify-content: space-between;
`
const StDetailTitleDiv = styled.div`
    display: flex;
    height: 80px;
    -webkit-box-pack: justify;
    justify-content: space-between;
    padding: 0px 24px;
    -webkit-box-align: center;
    align-items: center;
`
const StBackButton = styled.button`
    border: 1px solid rgb(221, 221, 221);
    height: 40px;
    width: 120px;
    background-color: rgb(255, 255, 255);
    border-radius: 12px;
    cursor: pointer;
`
const StDetailH1 = styled.h1`
    padding: 0px 24px;
    display: block;
    font-size: 2em;
    margin-block-start: 0.67em;
    margin-block-end: 0.67em;
    margin-inline-start: 0px;
    margin-inline-end: 0px;
    font-weight: bold;
`
const StDetailMain = styled.main`
        padding: 0px 24px;
        display: block;
`

const Detail = () => {

    const [selTodo] = useSelector(state => state.todoList);
    const { id } = useParams();
    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(getDetail(Number(id)))
    }, [dispatch, id])

    return (
        <StDetailWrapDiv>
            <StDetailBorderDiv>
                <div>
                    <StDetailTitleDiv>
                        <div>ID : {selTodo.id}</div>
                        <Link to="/">
                            <StBackButton>이전으로</StBackButton>
                        </Link>
                    </StDetailTitleDiv>
                    <StDetailH1>{selTodo.title}</StDetailH1>
                    <StDetailMain>{selTodo.content}</StDetailMain>
                </div>
            </StDetailBorderDiv>
        </StDetailWrapDiv>
    );
};

export default Detail;

상세페이지

 

상세 페이지 갔다가 돌아오면 한개만 남음...

그래서 별짓을 다 해봄...

결론은 설계 미스...

state를 todoList랑 todo를 갖는 객체로 만들어야 겠다는 생각밖에 안떠오름... 3시간은 뻘짓한듯..

 

 

//action 타입
const ADD_TODO = 'ADD_TODO';
const CHANGE_DONE = 'CHANGE_DONE';
const DELETE_TODO = 'DELETE_TODO';
const GET_DETAIL = 'GET_DETAIL';

//action 함수
export const addTodo = (obj) => {
    return {
        type: ADD_TODO,
        id: obj.id,
        title: obj.title,
        content: obj.content,
        done: false
    };
}

export const changeDone = (id) => {
    return {
        type: CHANGE_DONE,
        id: id
    }
}

export const deleteTodo = (id) => {
    return {
        type: DELETE_TODO,
        id: id
    }
}

export const getDetail = (id) => {
    return {
        type: GET_DETAIL,
        id: id
    }
}

// 초기 상태값
const initTodoList = {
    todoList: [
        {
            id: 0,
            title: `리액트 공부하기`,
            content: `리액트 기초를 공부해봅시다.`,
            done: false,
        },
        {
            id: 1,
            title: `리액트 공부하기2`,
            content: `리액트 기초를 공부해봅시다2.`,
            done: true,
        }
    ],
    todo: {
        id: 1,
        title: `리액트 공부하기2`,
        content: `리액트 기초를 공부해봅시다2.`,
        done: true,
    }
};
// 리듀서
const todoList = (state = initTodoList, action) => {
    switch (action.type) {
        case ADD_TODO:
            return {
                ...state,
                todoList: [
                    ...state.todoList,
                    {
                        id: state.todoList.length === 0 ? 0 : action.id,
                        title: action.title,
                        content: action.content,
                        done: action.done
                    }
                ]
            };
        case CHANGE_DONE:
            state.todoList.map((val, i, arr) => {
                if (val.id === action.id) {
                    arr[i].done ? arr[i].done = false : arr[i].done = true
                }
            })
            return { ...state, todoList: [...state.todoList] };
        case DELETE_TODO:
            state.todoList.map((val, i, arr) => {
                if (val.id === action.id) {
                    arr.splice(i, 1);
                }
            })
            return { ...state, todoList: [...state.todoList] };
        case GET_DETAIL:
            const [selTodo] = state.todoList.filter((val) => {
                return (val.id === action.id);
            })
            return { ...state, todo: selTodo };
        default:
            return state;
    }
};

export default todoList;

상태값의 타입을 새로 설정했다.

리스트의 배열객체랑 객체...

 

 

 

잘 된다...

useNavigate도 써보고 별짓 다 했었는데...

설계미스... 눈물난다

그래도 해결되서 행복하다..

 

'과제' 카테고리의 다른 글

항해99 React 1주차 과제 컴포넌트화  (0) 2022.10.03
항해99 React 1주차 과제  (1) 2022.10.02
언어 과제  (1) 2022.09.23