[WIL 4주차] 항해 99 미니토이프로젝트 나만의 미니 사전 with 리액트

hoonie·2021년 6월 29일
0

WIL

목록 보기
4/7
post-thumbnail
post-custom-banner

이번 항해99 4주차는 기본을 쌓은 리액트로 개인 미니프로젝트를 만들어보는것이었다.

프로젝트 설명

프로젝트 기간 : 2021-06-28 ~ 2012-06-30
프로젝트명 : My Dictionary
사용 기술 : 리액트, 파이어베이스
사용 IDE : VS Code
프로젝트 설명 : 단어 추가하기 웹뷰에서 단어 등록시 등록한 단어들의 카드리스트 출력( 데이터는 파이어베이스의 DB 이용 )
기능 : 단어와 단어에 대한 설명 및 예시 등록, 등록된 단어 수정 및 삭제
필수 포함 기능 :

  • 게시글 목록을 화면에 그리기 (각각 뷰는 카드 뷰로 만들기)
  • 게시글 내의 예시는 파란 글씨로 보여주기
  • 게시글 목록을 리덕스에서 관리하기
  • 게시글 목록을 파이어스토어에서 가져오기
  • 게시글 작성에 필요한 input 3개를 ref로 관리하기
  • 작성한 게시글을 리덕스 내 게시글 목록에 추가하기
  • 게시글 목록을 파이어스토어에 저장하기

컴포넌트 :
1. App.js ( 부모 컴포넌트 )
2. MyDictionary.js ( 등록된 단어 리스트 출력 )
3. AddWord.js ( 단어 추가 및 수정 )
4. NotFound.js ( 등록된 경로 접속시 필요한 잘못된 페이지 )

리덕스 모듈 :
1. dictionary.js ( reducer와 dispatch 하기 위한 함수, 파이어베이스 통신 함수 )
2. configStore.js ( 비동기 파이어베이스를 위한 미들웨어, createStore )

파이버에이스 설정 :
1.firebase.js (apiKey, authDomain 등 필요한 개인정보 입력)

프로젝트 와이어 프레임 :



실제 결과물 :


작성 코드


App.js


import "./App.css";
import MyDictionary from "./MyDictionary";
import { Route, Switch } from "react-router-dom";
import AddWord from "./AddWord";
import styled from "styled-components";
import NotFound from "./NotFound";

function App() {
  return (
    <div className="App">
      <Container>
        <Wrapper>
          <Switch>
            <Route exact path="/" component={MyDictionary}></Route>
            <Route path="/word/:type" component={AddWord}></Route>
            <Route component={NotFound}></Route>
          </Switch>
        </Wrapper>
      </Container>
    </div>
  );
}
const Container = styled.div`
  background-color: skyblue;
  max-width: 50vw;
  margin: auto;
`;

const Wrapper = styled.div`
  padding: 20px 10px;
  position: relative;
`;

export default App;

1.단어 추가 컴포넌트에 url 파라미터 전송 ( add or update ) -> add면 단어 추가 update면 등록된 단어 수정


MyDictionary.js


import React, { useEffect } from "react";
import styled from "styled-components";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { Button, ButtonGroup } from "@material-ui/core";
import { loadDictionaryFB, deleteBucketFB } from "./redux/module/dictionary";
import { useHistory } from "react-router-dom";

const MyDictionary = (props) => {
  const history = useHistory();
  const dispatch = useDispatch();
  const dic_list = useSelector((state) => state.dictionary.list);

  const deleteWord = (id) => {
    dispatch(deleteBucketFB(id));
  };

  useEffect(() => {
    dispatch(loadDictionaryFB());
  }, []);

  const updateWord = (selected_id) => {
    const selected_word = dic_list.find(({ id }) => {
      return id === selected_id;
    });

    history.push({
      pathname: "/word/update",
      state: {
        selected_word: selected_word,
      },
    });
  };
  return (
    <>
      <Title>MY DICTIONARY</Title>
      {dic_list.length === 0 ? (
        <ContentBox>
          <Content>
            <ContentWord>
              현재 추가된 단어가 존재하지 않습니다.
              <br></br>
              오른쪽 플러스 버튼을 눌러 사전을 등록해주세요.
            </ContentWord>
          </Content>
        </ContentBox>
      ) : (
        dic_list.map((props, index) => {
          return (
            <ContentBox key={props.id}>
              <Content>
                <ContentTitle>단어</ContentTitle>
                <ContentWord>{props.word}</ContentWord>
              </Content>
              <Content>
                <ContentTitle>설명</ContentTitle>
                <ContentWord>{props.desc}</ContentWord>
              </Content>
              <Content>
                <ContentTitle>예시</ContentTitle>
                <ContentWord color={"blue"}>{props.example}</ContentWord>
                <ButtonGroup
                  color="primary"
                  aria-label="outlined primary button group"
                  style={{ position: "absolute", right: "20px", top: "20px" }}
                >
                  <Button
                    onClick={() => {
                      updateWord(props.id);
                    }}
                  >
                    수정
                  </Button>
                  <Button
                    onClick={() => {
                      deleteWord(props.id);
                    }}
                  >
                    삭제
                  </Button>
                </ButtonGroup>
              </Content>
            </ContentBox>
          );
        })
      )}
      <Link to="/word/add">
        <PlusBtn>+</PlusBtn>
      </Link>
    </>
  );
};

const Title = styled.h4``;

const ContentBox = styled.div`
  background-color: #fff;
  width: 100%;
  padding: 5px;
  box-sizing: border-box;
  margin: 15px 0;
  position: relative;
`;

const Content = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  &:nth-child(odd) {
    margin: 20px 0;
  }
`;

const ContentTitle = styled.div`
  font-size: 0.8rem;
  text-decoration: underline;
`;

const ContentWord = styled.div`
  font-size: 1.1rem;
  color: ${(props) => (props.color ? "blue" : "black")};
`;

const PlusBtn = styled.div`
  width: 50px;
  height: 50px;
  background-color: black;
  border-radius: 50%;
  color: #fff;
  position: absolute;
  right: 10px;
  bottom: 10px;
  line-height: 42px;
  text-align: center;
  font-size: 50px;
  cursor: pointer;
`;
export default MyDictionary;

1.단어리스트가 없다면 추가된 단어가 존재하지 않는다는 안내문구 출력
2.useSelector를 이용하여 리덕스 state값을 변수로 담은후 map을 이용하여 단어 리스트 출력
3.수정버튼 클릭시 해당 단어의 state값을 selected_word 상태값에 담아서 word/update로 전송
4.플러스 버튼 클릭시 word/add로 이동


AddWord.js


import React, { useRef, useEffect } from "react";
import styled from "styled-components";
import { useDispatch } from "react-redux";
import { createDictionaryFB, updateBucketFB } from "./redux/module/dictionary";
import { useHistory, useLocation } from "react-router-dom";

const AddWord = ({ match }) => {
  const wordInput = useRef();
  const descInput = useRef();
  const exampleInput = useRef();
  const location = useLocation();

  const history = useHistory();
  const dispatch = useDispatch();

  useEffect(() => {
    if (location.state !== undefined) {
      const { word, desc, example } = location.state.selected_word;

      wordInput.current.value = word;
      descInput.current.value = desc;
      exampleInput.current.value = example;
    }
  }, []);

  let typeTitle;
  if (match.params.type === "update") {
    typeTitle = "수정";
  } else {
    typeTitle = "추가";
  }

  const addWord = () => {
    const new_list_state = {
      word: wordInput.current.value,
      desc: descInput.current.value,
      example: exampleInput.current.value,
    };

    if (match.params.type === "update") {
      let id = location.state.selected_word.id;
      dispatch(updateBucketFB(id, new_list_state));
    } else {
      dispatch(createDictionaryFB(new_list_state));
    }
    history.goBack();
  };

  return (
    <>
      <Title>단어 {typeTitle}하기</Title>
      <ContentBox>
        <Content>
          <ContentTitle>단어</ContentTitle>
          <ContentInput name="word" ref={wordInput} />
        </Content>
        <Content>
          <ContentTitle>설명</ContentTitle>
          <ContentInput name="desc" ref={descInput} />
        </Content>
        <Content>
          <ContentTitle>예시</ContentTitle>
          <ContentInput name="example" ref={exampleInput} />
        </Content>
      </ContentBox>
      <AddBtn
        onClick={() => {
          addWord();
        }}
      >
        {typeTitle}하기
      </AddBtn>
    </>
  );
};

const Title = styled.h4``;

const ContentBox = styled.div`
  background-color: #fff;
  width: 100%;
  padding: 5px;
  box-sizing: border-box;
  margin: 15px 0;
`;

const Content = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  &:nth-child(odd) {
    margin: 20px 0;
  }
`;

const ContentTitle = styled.div`
  font-size: 0.8rem;
  text-decoration: underline;
`;

const ContentInput = styled.input`
  height: 30px;
  margin-top: 10px;
`;

const AddBtn = styled.div`
  width: 100%;
  box-sizing: border-box;
  height: 50px;
  background-color: purple;
  color: #fff;
  text-align: center;
  font-size: 30px;
  cursor: pointer;
  line-height: 50px;
  margin-top: 40px;
`;

export default AddWord;
  1. 넘어온 상태값 (selected_word) 가 있다면 input value에 해당 값 출력
  2. 넘어온 params 값이 update라면 단어 추가란 단어 대신 단어 수정으로 출력
  3. useRef를 이용한 돔 조작을 하여 각 input의 value를 받아 변수 생성 후 리덕스 dispatch를 활용하여 생성한 함수 호출

redux/module/dictionary.js


import { firestore } from "../../firebase";

const LOAD = "dictionary/LOAD";
const CREATE = "dictionary/CREATE";
const DELETE = "dictionary/DELETE";
const UPDATE = "dictionary/UPDATE";

const initailState = {
  list: [
    { id: "1", word: "안녕하세요", desc: "설명입니다", example: "예시입니다" },
    {
      id: "2",
      word: "안녕하세요1",
      desc: "설명입니다1",
      example: "예시입니다",
    },
    {
      id: "3",
      word: "안녕하세요2",
      desc: "설명입니다2",
      example: "예시입니다1",
    },
  ],
};

const dictionary_db = firestore.collection("dictionary");

export const loadDictionary = (dictionary) => {
  return {
    type: LOAD,
    dictionary,
  };
};

export const createDictionary = (dictionary) => {
  return {
    type: CREATE,
    dictionary,
  };
};

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

export const updateDictionary = (id) => {
  return {
    type: UPDATE,
    id,
  };
};

export const loadDictionaryFB = () => {
  return function (dispatch) {
    dictionary_db.get().then((docs) => {
      let dictionary_data = [];
      docs.forEach((doc) => {
        if (doc.exists) {
          dictionary_data = [...dictionary_data, { id: doc.id, ...doc.data() }];
        }
      });
      dispatch(loadDictionary(dictionary_data));
    });
  };
};

export const createDictionaryFB = (dictionary) => {
  return function (dispatch) {
    let dictionary_data = dictionary;

    dictionary_db
      .add(dictionary_data)
      .then((docRef) => {
        dictionary_data = { ...dictionary_data, id: docRef.id };
      })
      .catch((err) => {
        alert("create err");
      });
  };
};

export const deleteBucketFB = (id) => {
  return function (dispatch) {
    dictionary_db
      .doc(id)
      .delete()
      .then((res) => {
        dispatch(deleteDictionary(id));
      });
  };
};

export const updateBucketFB = (id, dictionary) => {
  return function (dispatch) {
    dictionary_db.doc(id).update(dictionary);
  };
};

const reducer = (state = initailState, action) => {
  switch (action.type) {
    case LOAD: {
      if (action.dictionary.length > 0) {
        return { list: action.dictionary };
      }
      return state;
    }
    case CREATE: {
      const new_list = [...state.list, action.dictionary];
      return { list: new_list };
    }
    case DELETE: {
      const new_list = state.list.filter(({ id }) => {
        return id !== action.id;
      });

      return {
        list: new_list,
      };
    }
    default:
      return state;
  }
};

export default reducer;

1.LOAD가 발생하지 않을시 발생되는 초기 상태값 지정
2. CRUD 를 위한 dispatch 함수 및 reducer 생성


configStore.js


import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import { createBrowserHistory } from "history";
import dictionary from "./module/dictionary";
import thunk from "redux-thunk";

export const history = createBrowserHistory();
const middlewares = [thunk];

const enhancer = applyMiddleware(...middlewares);

const rootReducer = combineReducers({ dictionary });

const store = createStore(rootReducer, enhancer);

export default store;
  1. 비동기 firebase 통신을 위한 미들웨어 redux-thunk 생성
  2. reducer 함수들을 결합하고 store로 만들기 위한 combineReducers 및 createStore 생성

느낀점

기본 개념을 익히고 나서 바로 프로젝트에 적용시켜보았다.
강의 영상을 볼때는 어렵지않게 느껴졌고 바로 적용할 수 있을것 같았으나, 막상 프로젝트를 진행하니 뇌정지라 일어나고 필요한 설정도 깜빡하고 진행안한 경우도 있었고 참 간단한거 같으면서도 삽질을 은근 많이한것 같다.

역시 개발이라는것은 실제로 프로젝트를 진행하면서 실력이 많이 느는것 같다. 백날 이론이나 강의영상 보면 금방 까먹게되고 실무에 적용을 못하게 되는 케이스가 빈번하게 일어나게 될 것같다.

앞으로 많은 프로젝트를 진행해나가면서 리액트의 감을 익히면서 능숙한 주니어 개발자가 되고 싶다!

감사합니다 :)


github url

project url

post-custom-banner

0개의 댓글