Wish List Project

thisisyjin·2022년 6월 5일
0

Dev Log 🐥

목록 보기
21/23
post-thumbnail

💫 Wish-List Project

  • redux와 localStorage로 상태 관리 및 저장.
  • react 컴포넌트 기반 styled-components 스타일링.
  • react-router-dom 기반 라우트 설정 + 컴포넌트 렌더링


🌐 배포 링크 - ❗️모바일 환경 최적화



🖼 Preview

📱 Mobile

  • main

  • detail

🖥 Desktop


🎨 디자인 및 기획

  • 직접 디자인 툴인 figma로 디자인 및 기획 진행함.
  • 파비콘도 만들어 적용함

🚀 스택

  • React
  • Redux
  • JavaScript

📂 디렉터리 구조

├── public
└── src
    ├── components
    ├── routes
    ├── assets ── lib
    └── styles

주요 코드

store.js

import { createStore } from 'redux';

const ADD = 'ADD';
const DELETE = 'DELETE';
const CLEAR = 'CLEAR';

const addWish = (text) => {
  return {
    type: ADD,
    text,
  };
};

const deleteWish = (id) => {
  return {
    type: DELETE,
    id,
  };
};

const clearAll = () => {
  return {
    type: CLEAR,
  };
};

JSON.parse(localStorage.getItem('wishes')) ||
  localStorage.setItem('wishes', JSON.stringify([]));

const reducer = (
  state = JSON.parse(localStorage.getItem('wishes')),
  action
) => {
  switch (action.type) {
    case ADD:
      const addItem = [...state, { text: action.text, id: Date.now() }];
      localStorage.setItem('wishes', JSON.stringify(addItem));
      return addItem;

    case DELETE:
      const deleteItem = state.filter((toDo) => toDo.id !== action.id);
      localStorage.setItem('wishes', JSON.stringify(deleteItem));
      return deleteItem;

    case CLEAR:
      localStorage.setItem('wishes', JSON.stringify([]));
      return [];

    default:
      return state;
  }
};

const store = createStore(reducer);

export const actionCreators = {
  addWish,
  deleteWish,
  clearAll,
};

export default store;
  • 만약 로컬스토리지에 wishes 필드가 존재한다면 그 값을 가져오고, 없다면 빈 배열을 기본값으로 설정하여 로컬스토리지에 등록함.

  • state의 기본값은 JSON.parse(localStorage.getItem('wishes')) = 배열

  1. ADD
  2. DELETE
  3. CLEAR
  • localStorage.setItem('wishes', JSON.stringify(바뀐state)
  • return (바뀐state)
  • reducer이 하나만 존재하므로 하나의 파일에 모든 리덕스 관련 코드를 작성했음.
  • 액션 생성함수를 (추후 mapDispatchToProps로 전달해줄 함수와 구분하기 위해서)
    하나의 객체로 묶어서 export 해줌.
  • 여기서는 액션생성함수와 리듀서함수를 정석대로 작성함.
    (redux-actions 라이브러리 사용시 더 편리)

Home.jsx

import { useState } from 'react';
import { connect } from 'react-redux';
import { actionCreators } from '../store';
import { Link } from 'react-router-dom';
import { Container, StyledHeader, StyledMain } from '../styles/HomeStyle';
import { ReactComponent as Plus } from '../assets/plus.svg';
import { ReactComponent as Clear } from '../assets/clear.svg';

import Wish from '../components/Wish';

const Home = ({ wishes, addWishes, clearAll }) => {
  const [text, setText] = useState('');
  const onChangeInput = (e) => {
    setText(e.target.value);
  };

  const onSubmitForm = (e) => {
    e.preventDefault();
    addWishes(text);
    setText('');
  };

  const clearLocal = () => {
    clearAll();
  };

  return (
    <>
      <Container>
        <StyledHeader>
          <h1 className="header-logo">
            <Link to="/">WISH LIST</Link>
          </h1>
          <form onSubmit={onSubmitForm}>
            <input
              type="text"
              value={text}
              onChange={onChangeInput}
              className="wish-input"
              placeholder="If you sincerely hope, It will come true."
            />
            <button className="wish-button">
              <Plus />
            </button>
          </form>
        </StyledHeader>
        <StyledMain>
          <ul className="wish-list">
            {wishes.map((wish) => (
              <Wish text={wish.text} id={wish.id} key={wish.id} />
            ))}
          </ul>
          <button className="clear-button" onClick={clearLocal}>
            <Clear />
          </button>
        </StyledMain>
      </Container>
    </>
  );
};

const mapStateToProps = (state) => {
  return { wishes: state };
};

const mapDispatchToProps = (dispatch) => {
  return {
    addWishes: (text) => {
      dispatch(actionCreators.addWish(text));
    },
    clearAll: () => {
      dispatch(actionCreators.clearAll());
    },
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Home);
  • 원래대로라면 useSelector, useDispatch 훅을 사용하는 것이 더 편리하지만,
    react-redux의 핵심 함수인 connect의 원리에 대해 이해하고자 이 방법을 선택함.
    -> HOC(Higher-order component)임.

  • connect의 첫번째 인자로 mapStateToProps가 들어감.
    -> store의 state를 wishes 라는 Home 컴포넌트의 props로 넣어줌.

  • addWish와 clearAll 함수를 dispatch 해서 액션을 발생시킴.
    -> reducer에 의해 state가 변경됨.

Detail.jsx

import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Container, StyledMain } from '../styles/HomeStyle';
import {
  StyledDetailHeader,
  StyledSpan,
  DetailInfo,
} from '../styles/DetailStyle';
import { actionCreators } from '../store';
import Deleted from '../components/Deleted';
import { ReactComponent as Delete } from '../assets/delete.svg';

const Detail = ({ wish, deleteTodo }) => {
  const { id } = useParams();
  const wishText = wish.find((wish) => wish.id === parseInt(id));

  const onClickButton = () => {
    deleteTodo(id);
    console.log(wish);
  };

  const writtenDate = new Date(+id);
  const year = writtenDate.getFullYear();
  const month = writtenDate.getMonth() + 1;
  const date = writtenDate.getDate();

  const writtenDateString = `${year}-${month.toString().padStart(2, '0')}-${date
    .toString()
    .padStart(2, '0')}`;

  return (
    <>
      <Container>
        <StyledDetailHeader>
          <h1 className="header-logo">
            <span className="header-span">
              {' '}
              “ If you sincerely hope, It will come true.</span>
            <Link to="/">WISH LIST</Link>
          </h1>
        </StyledDetailHeader>
        <StyledMain>
          {wishText ? (
            <>
              <StyledSpan>I wish ...</StyledSpan>
              <DetailInfo>
                <h2 className="wish-title">{wishText.text}</h2>
                <h5 className="wish-date">
                  <span className="date-title">written date </span>{' '}
                  {writtenDateString}
                </h5>
                <button
                  className="detail-delete-button"
                  onClick={onClickButton}
                >
                  <Delete />
                </button>
              </DetailInfo>
            </>
          ) : (
            <Deleted />
          )}
        </StyledMain>
      </Container>
    </>
  );
};

const mapStateToProps = (state) => {
  return {
    wish: state,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    deleteTodo: (id) => dispatch(actionCreators.deleteWish(+id)),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Detail);
  • id를 useParams()로 가져온 후, new Date(id)를 해서 작성 날짜를 렌더링함.

  • 만약 wishText가 없다면 -> 이미 state(=wish)에서 삭제된 필드임.
    -> Deleted 컴포넌트를 렌더링함.

  • 참고로, useParams의 결과는 숫자도 string이 됨.(주의!)

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글