이번 항해99 4주차는 기본을 쌓은 리액트로 개인 미니프로젝트를 만들어보는것이었다.
프로젝트 기간 : 2021-06-28 ~ 2012-06-30
프로젝트명 : My Dictionary
사용 기술 : 리액트, 파이어베이스
사용 IDE : VS Code
프로젝트 설명 : 단어 추가하기 웹뷰에서 단어 등록시 등록한 단어들의 카드리스트 출력( 데이터는 파이어베이스의 DB 이용 )
기능 : 단어와 단어에 대한 설명 및 예시 등록, 등록된 단어 수정 및 삭제
필수 포함 기능 :
컴포넌트 :
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 등 필요한 개인정보 입력)
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면 등록된 단어 수정
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로 이동
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;
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 생성
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;
기본 개념을 익히고 나서 바로 프로젝트에 적용시켜보았다.
강의 영상을 볼때는 어렵지않게 느껴졌고 바로 적용할 수 있을것 같았으나, 막상 프로젝트를 진행하니 뇌정지라 일어나고 필요한 설정도 깜빡하고 진행안한 경우도 있었고 참 간단한거 같으면서도 삽질을 은근 많이한것 같다.
역시 개발이라는것은 실제로 프로젝트를 진행하면서 실력이 많이 느는것 같다. 백날 이론이나 강의영상 보면 금방 까먹게되고 실무에 적용을 못하게 되는 케이스가 빈번하게 일어나게 될 것같다.
앞으로 많은 프로젝트를 진행해나가면서 리액트의 감을 익히면서 능숙한 주니어 개발자가 되고 싶다!
감사합니다 :)
github url
project url