[미니 토이 프로젝트] React-Redux 이용해서 단어장 만들기 (1)

devMag 개발 블로그·2022년 2월 3일
0

사이드 프로젝트

목록 보기
1/3
post-custom-banner

프로젝트 설명

React와 Redux를 사용해서 CRUD를 구현한 단어장 웹 페이지 만들어보기
(D는 구현 못함)

github 주소

week4-dictionary

사용 기술 및 라이브러리

  • React
    • creat-react-app
    • react router dom
  • Redux
    • redux-persist
    • redux thunk
  • styled-components
  • Material UI
  • firebase

프로젝트

메인페이지

  • 나만의 단어장 제목
  • 카드들을 불러와서 나열하기
  • 단어 추가하기 버튼. 클릭 시 추가하기 페이지로 이동

메인페이지

카드

  • 단어 / 설명 / 예문을 보여줌
  • completed 버튼 클릭 시 공부를 한 카드와 공부를 안 한 카드에 따라 구분 바탕색으로 구분감 줌
  • modify 버튼을 클릭 시 해당 카드 수정 페이지로 이동

카드

추가하기 페이지

  • 단어, 설명, 예문을 입력할 수 있는 페이지
  • 추가하기 버튼 클릭 시 추가했다는 alert 창 이후 메인페이지로 이동

추가하기

해당 카드 수정하기 페이지

  • 해당 카드의 원래 적혀 있던값이 그대로 input 칸에 적혀 있으면 수정해서 적을 수 있음
  • 수정하기 버튼 클릭 시 alert창으로 알려주며 메인페이지로 이동

수정하기


코드

  • 스타일 부분은 너무 길어지기에 제외
  • 기능을 위한 주요 부분들만 올려놓음

App.js

메인 화면단. 카드 컴포넌트를 갖고와서 뿌려준다.

import styled from "styled-components";
import { Route, Routes } from 'react-router-dom';
import { useSelector } from 'react-redux';
import Card from './Card';
import AddWord from './AddWord';
import Modify from './Modify';
// 로딩 시간중 화면을 가려준다.
import Spinner from './Spinner';

function App() {
	// 리덕스의 상태중에 is_loaded의 값을 갖고온다.
	// 초깃값에 is_loaded = false; 값을 주면 데이터를 받아서 화면에 뿌려지기 전까지는
	// is_loaded 값이 false값이다. 
	const is_loaded = useSelector(state => state.word.is_loaded);

	return (
		<>
			<TitleStyled>나만의 단어장</TitleStyled>
			<ContainerStyled>
				{/* react-router-dom v6라서 Swich가 아니라 Routes로 감싸준다. */}
				<Routes>
					<Route path="/" element={<Card />} />
					<Route path="/add" element={<AddWord />} />
					<Route path="/modify/:index" element={<Modify />} />
				</Routes>
			</ContainerStyled>
			{/* is_loaded의 값이 false일 때는 아직 데이터를 모두 받아온 게 아니다.
			!is_loaded면 true가 되므로 Spinner 컴포넌트 호출해서 화면 가리기 */}
			{!is_loaded && <Spinner />}
		</>
	);
}

const TitleStyled = styled.h1`
...
`;

const ContainerStyled = styled.div`
...
`;


export default App;

Card.js

카드 하나에 대한 컴포넌트

import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { loadWordFB, completedWordFB } from './redux/modules/word';
import Button from '@mui/material/Button';
import SendIcon from '@mui/icons-material/Send';
import SaveIcon from '@mui/icons-material/Save';



const Card = () => {
    // dic_list 라는 이름으로 리덕스에서 데이터 리스트를 받아온다.
    const dic_list = useSelector((state) => state.word.list);
    const dispatch = useDispatch();

    // 처음 페이지를 열었을 시 dispatch로 loadWordFB()를 실행시킨다.
    // 파이어스토어에서 데이터를 받아온 다음에 화면에 뿌려준다.
    // load를 하면서 is_loaded값도 true로 변경해준다.
    React.useEffect(() => {
        dispatch(loadWordFB());
    }, [dispatch]);

    // completed 값을 true, false 변경하는 함수
    const completedWord = async (dic) => {
        await dispatch(completedWordFB(dic))
    }

    return (
        <>
            {dic_list &&
                // 리덕스에서 받아온 데이터 리스트를 map을 돌려서 dic이란 이름으로 하나씩 꺼내온다.
                dic_list.map((dic, idx) => {
                    return (
                        // 삼항연산자를 이용해서 데이터에서 completed 값이 true일 경우와
                        // false일 경우에 따라 스타일을 다르게 준다.
                        dic.completed === true
                            ?
                            <CardStyled key={dic.id}>
                                <span>단어</span>
                                <p className="word">{dic.word}</p>
                                <span>설명</span>
                                <p className="content">{dic.content}</p>
                                <span>예문</span>
                                <p className="explain">{dic.explain}</p>

                                {/* 버튼 클릭 시 completedWord 함수에 dic 데이터를 파라미터로 넘겨준다.*/}
                                <Button variant="contained" color="success" onClick={() => {
                                    completedWord(dic)
                                }}>공부함</Button>

                                {/* Link를 이용해서 해당 카드의 인덱스값을 기준으로 이동시킨다. */}
                                <Link to={`/modify/${idx}`} style={{ textDecoration: 'none' }}>
                                    <Button variant="contained" endIcon={<SendIcon />}>
                                        수정하기
                                    </Button>
                                </Link>

                            </CardStyled>
                            :
                            <CompletedCardStyled key={dic.id}>
                                <span>단어</span>
                                <p className="word">{dic.word}</p>
                                <span>설명</span>
                                <p className="content">{dic.content}</p>
                                <span>예문</span>
                                <p className="explain">{dic.explain}</p>

                                <Button variant="contained" onClick={() => {
                                    completedWord(dic)
                                }}>생각해보니 공부 안함</Button>

                                <Link to={`/modify/${idx}`} style={{ textDecoration: "none" }}>
                                    <Button variant="contained" endIcon={<SendIcon />} >
                                        수정하기
                                    </Button>
                                </Link>
                            </CompletedCardStyled>
                    )
                })
            }
            {/* 해당 버튼 클릭 시 단어 추가 페이지로 이동한다. */}
            <Link to="/add" style={{ textDecoration: "none" }}>
                <AddBtnStyled>
                    <Button color="secondary" endIcon={<SaveIcon />} variant="contained">
                        단어 추가하기
                    </Button>
                </AddBtnStyled>
            </Link>
        </>
    )
}

const CardStyled = styled.div`
...
`;

const CompletedCardStyled = styled.div`
...
`;

const AddBtnStyled = styled.div`
...
`;

export default Card;

Addword.js

카드를 추가하는 페이지 컴포넌트

import React from 'react';
import styled from 'styled-components';
import { createWordFB } from './redux/modules/word';
import { useDispatch } from 'react-redux';
import { useNavigate } from "react-router-dom";


const AddWord = () => {
    const dispatch = useDispatch();
    const inputWord = React.useRef(null);
    const inputContent = React.useRef(null);
    const inputExplain = React.useRef(null);
    const navigate = useNavigate();


    const addWordList = () => {
        dispatch(createWordFB(
            {
                word: inputWord.current.value,
                content: inputContent.current.value,
                explain: inputExplain.current.value,
                completed: false,
            }
        ));

        window.alert("단어가 추가되었습니다.");
        // 주소 "/" 로 이동한다.
        navigate("/");
    }

    return (
        <>
            <AddWordStyled>
                <span>단어</span>
                <input className="word" ref={inputWord}></input>
                <span>설명</span>
                <input className="content" ref={inputContent}></input>
                <span>예문</span>
                <input className="explain" ref={inputExplain}></input>
                <button onClick={addWordList}>추가하기</button>
            </AddWordStyled>
        </>
    )
}

const AddWordStyled = styled.div`
 ...
`;

export default AddWord;

Modify.js

해당 카드의 내용을 수정하는 페이지 컴포넌트

import React from 'react';
import styled from 'styled-components';
import { useNavigate, useParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { updateWordFB } from './redux/modules/word';

const Modify = () => {
    const dispatch = useDispatch();
    const navigate = useNavigate();
    const { index } = useParams();
    const { id, word, content, explain } = useSelector(state => state.word.list[index])

    const inputWord = React.useRef(null);
    const inputContent = React.useRef(null);
    const inputExplain = React.useRef(null);

    const ModifyWordList = () => {
        dispatch(updateWordFB({
            id: id,
            word: inputWord.current.value,
            content: inputContent.current.value,
            explain: inputExplain.current.value,
        }))

        window.alert("단어를 수정했습니다.");
        navigate("/");
    }


    return (
        <>
            <ModifyStyled>
                <span>단어</span>
                <input className="word" ref={inputWord} defaultValue={word}></input>
                <span>설명</span>
                <input className="content" ref={inputContent} defaultValue={content}></input>
                <span>예문</span>
                <input className="explain" ref={inputExplain} defaultValue={explain}></input>
                <button onClick={ModifyWordList}>수정하기</button>
            </ModifyStyled>
        </>
    )
}

const ModifyStyled = styled.div`
...
`;

export default Modify;

redux/modules/word.js

리덕스의 action, action create function, reducer, middleware에 대한 부분들을 모아놓은 파일

import { doc, collection, getDocs, addDoc, updateDoc } from "firebase/firestore";
import { db } from '../../firebase';

const initialState = {
    // 맨 처음에 is_loaded를 false로 줘서 Spinner 컴포넌트가 화면을 가리게 해준다.
    is_loaded: false,
    list: []
}

// Actions
const LOAD = 'words/LOAD';
const CREATE = 'words/CREATE';
const COMPLETED = 'words/COMPLETED';
const UPDATE = 'words/UPDATE';


// Action Creators
export function loadWord(word_list) {
    return { type: LOAD, word_list };
}

export function createWord(word_data) {
    return { type: CREATE, word_data };
}

export function completedWord(word_index) {
    return { type: COMPLETED, word_index };
}

export function updateWord(word) {
    return { type: UPDATE, word };
}


// middlewares
export function loadWordFB() {
    return async function (dispatch) {
        const word_data = await getDocs(collection(db, "words"));
        let word_list = [];
        word_data.forEach((doc) => {
            word_list.push({ id: doc.id, ...doc.data() });
        })

        dispatch(loadWord(word_list));
    }
}

export function createWordFB(word) {
    return async function (dispatch) {
        const docRef = await addDoc(collection(db, "words"), word);
        const word_data = { id: docRef.id, ...word };
        dispatch(createWord(word_data));
    }
}

export function completedWordFB(word) {
    return async function (dispatch, getState) {
        const docRef = doc(db, "words", word.id);
        await updateDoc(docRef, { completed: !word.completed });

        const word_list = getState().word.list;
        const word_index = word_list.findIndex((w) => {
            return w.id === word.id;
        })

        dispatch(completedWord(word_index));
        dispatch(loadWordFB());
    }
}

export function updateWordFB(word, id) {
    return async function (dispatch) {
        const docRef = doc(db, "words", word.id);
        await updateDoc(docRef, {
            word: word.word,
            content: word.content,
            explain: word.explain,
        });
        const new_word = { ...word, id };

        dispatch(updateWord(new_word));
    }
}


// Reducer
export default function reducer(state = initialState, action = {}) {
    switch (action.type) {
        case "words/LOAD":
            return { list: action.word_list, is_loaded: true };

        case "words/CREATE": {
            const new_word_list = [...state.list, action.word_data];
            console.log(new_word_list)
            return { list: new_word_list, ...state };
        }

        case "words/COMPLETED": {
            const new_word_list = state.list.map((word) => {
                return word.id === action.word_index
                    ? { ...word, completed: !word.completed }
                    : word
            });
            return { ...state, list: new_word_list };
        }

        case "words/UPDATE": {
            const new_word_list = state.list.map((word) => {
                return word.id === action.word.id
                    ? { ...word, ...action.word }
                    : word
            })
            return { ...state, list: new_word_list };
        }

        default: return state;
    }
}
profile
최근 공부 내용 정리 Notion Link : https://western-hub-b8a.notion.site/Study-5f096d07f23b4676a294b2a2c62151b7
post-custom-banner

0개의 댓글