2차 공부/TIL

24.08.13 개인과제 리팩토링

공대탈출 2024. 8. 13. 20:17
import React, { useEffect, useRef, useState } from "react";
import Title from "./container/Title.jsx";
import MedalListBox from "./container/MedalListBox.jsx";
import "./Container.css";
import MedalInputForm from "./container/MedalInputForm.jsx";
import Modal from "./container/Modal.jsx";
import useDidMountedEffect from "../hooks/useDidMountedEffect.jsx";

const Container = () => {
    const modalRef = useRef();

    const [total, setTotal] = useState([]);
    const [country, setCountry] = useState("");
    const [goldMedal, setGoldMedal] = useState(0);
    const [silverMedal, setSilverMedal] = useState(0);
    const [bronzeMedal, setBronzeMedal] = useState(0);
    const [alertText, setAlertText] = useState("");

    const medals = [
        { key: new Date().getTime() + 1, title: "금메달", value: goldMedal, setValue: setGoldMedal },
        { key: new Date().getTime() + 2, title: "은메달", value: silverMedal, setValue: setSilverMedal },
        { key: new Date().getTime() + 3, title: "동메달", value: bronzeMedal, setValue: setBronzeMedal },
    ];
    const sortCountries = (array) => {
        return array.sort((a, b) => {
            if (b.gold !== a.gold) return b.gold - a.gold;
            else if (b.silver !== a.silver) return b.silver - a.silver;
            return b.bronze - a.bronze;
        });
    };
    const setModal = (text) => {
        setAlertText(text);
        modalRef.current.showModal();
    };
    const setToInitial = () => {
        setCountry("");
        setGoldMedal(0);
        setSilverMedal(0);
        setBronzeMedal(0);
    };
    const check = (isupdating = false) => {
        const trimmedName = country.trim();
        if (!trimmedName) {
            setModal("국가명을 입력해주세요");
            return true;
        }
        const index = total.findIndex((el) => el.name === country);
        if (isupdating && index === -1) {
            setModal("등록되지 않은 국가입니다.");
            return true;
        }
        if (!isupdating && index !== -1) {
            setModal("이미 존재하는 국가입니다.");
            return true;
        }
    };
    const addHandler = (e) => {
        e.preventDefault();
        if (check()) return null;
        const id = new Date().getTime();
        const newCountry = {
            id: id,
            name: country,
            gold: goldMedal,
            silver: silverMedal,
            bronze: bronzeMedal,
        };
        setTotal(sortCountries([...total, newCountry]));
        setToInitial();
    };
    const updateHandler = (e) => {
        e.preventDefault();
        if (check(true)) return null;
        const copyTotal = [...total].map((el) => {
            return el.name === country ? { ...el, gold: goldMedal, silver: silverMedal, bronze: bronzeMedal } : el;
        });
        setTotal(sortCountries(copyTotal));
        setToInitial();
    };
    const deleteHandler = (e) => {
        const { id } = e.target;
        const newArray = [...total].filter((el) => el.id != id);
        setTotal(newArray);
    };

    useEffect(() => {
        if (localStorage.getItem("countries")) {
            const countries = JSON.parse(localStorage.getItem("countries"));
            setTotal(countries);
        }
    }, []);
    const setLocalStorage = () => {
        localStorage.setItem("countries", JSON.stringify(total));
    };
    useDidMountedEffect(setLocalStorage, total);

    return (
        <>
            <div className="wrapper">
                <Title />
                <MedalInputForm
                    country={country}
                    setCountry={setCountry}
                    medals={medals}
                    addHandler={addHandler}
                    updateHandler={updateHandler}
                />
                {total.length ? (
                    <MedalListBox onClick={deleteHandler} total={total} />
                ) : (
                    <p className="blankMedalList">아직 추가된 국가가 없습니다. 국가를 추가해주세요!</p>
                )}
            </div>
            <Modal elRef={modalRef} alertText={alertText} />
        </>
    );
};

export default Container;

 

처음 state를 메달별로 나눠놓은 것이 매우 큰 문제가 되었다.

import React from "react";
import Input from "../common/Input.jsx";

export const inputTitleStyle = {
    textAlign: "center",
};
export const inputBoxDiv = {
    display: "flex",
    flexDirection: "column",
    gap: "5px",
};

const CountryInput = ({ value, setValue }) => {
    return (
        <div style={inputBoxDiv}>
            <label htmlFor="countryInput" style={inputTitleStyle}>
                국가명
            </label>
            <Input placeholder="국가입력" id="countryInput" type="text" value={value} setValue={setValue} />
        </div>
    );
};

export default CountryInput;
import React from "react";
import CountryInput from "./CountryInput.jsx";
import MedalInput from "./MedalInput.jsx";
import Button from "../common/Button.jsx";

const MedalInputForm = ({ country, setCountry, medals, addHandler, updateHandler }) => {
    return (
        <form className="inputGroup">
            <CountryInput value={country} setValue={setCountry} />
            <MedalInput medals={medals} />
            <div className="buttonGroup">
                <Button name="mainBtn" onClick={addHandler}>
                    국가 추가
                </Button>
                <Button name="mainBtn" onClick={updateHandler}>
                    업데이트
                </Button>
            </div>
        </form>
    );
};

export default MedalInputForm;
import React from "react";
import Input from "../common/Input.jsx";
import { inputTitleStyle } from "./CountryInput.jsx";
import { inputBoxDiv } from "./CountryInput.jsx";

const MedalInput = ({ medals }) => {
    return (
        <>
            {medals.map(({ key, title, value, setValue }) => {
                return (
                    <div style={inputBoxDiv} key={key}>
                        <label htmlFor={key} style={inputTitleStyle}>
                            {title}
                        </label>
                        <Input id={key} type="number" value={value} setValue={setValue} />
                    </div>
                );
            })}
        </>
    );
};

export default MedalInput;
import "./Input.css";

const Input = ({ type = "number", value, setValue, placeholder, id }) => {
    const inputHandler = (e) => {
        const { type, value } = e.target;
        type === "text" ? setValue(value) : setValue(Number(value));
    };

    return <input placeholder={placeholder} id={id} type={type} onChange={inputHandler} value={value} />;
};

export default Input;

대공사가 필요하다.

 

처음 만들어야하는 페이지를 테스트해보며 컴포넌트를 나누고 생각속에서 나눈 컴포넌트를 기준으로 state도 나누어 만들다보니 컴포넌트가 가진 기능, 의미, 행동에 따른 분리가 아니라 그저 분리를 위한 분리로 컴포넌트를 분리했다.

그렇게 나눠진 컴포넌트에 맞춰 state를 마구 쪼개버리다보니 결국 같은 로직으로 묶이는 state들인데 나눠놓아 리팩토링을 제안받았다.

또한 공통컴포넌트로 사용하는 Input컴포넌트의 onChange가 setValue를받아 setState를 하는 것으로 고정되어있다보니 Input의 역할이 갇혀버렸다는 피드백도 받았다.

 

일단먼저 country, gold, silver, bronze를 한번에 관리할 state를 만들고, 해당값을 추가할 countries state를 만들었다.

기존에 작성한 코드를 모두 뒤집어가며 리팩토링하기엔 엄두가 나지 않았기 떄문에 새로운 컴포넌트를 만들고, 새로운 state를 적용시키며 리팩토링 후 삭제하는 방식으로 진행했다.

const [countries, setCountries] = useState([]);
const [form, setForm] = useState(INITIAL_STATE);

const formCheck = (isupdating = false) => {
    const trimmedName = form.country.trim();
    if (!trimmedName) {
        setModal("국가명을 입력해주세요");
        return true;
    }
    if (form.gold < 0 || form.silver < 0 || form.bronze < 0) {
        setModal("메달수는 0이상만 입력가능합니다.");
        return true;
    }
    const index = countries.findIndex((el) => el.country === form.country);
    if (isupdating && index === -1) {
        setModal("등록되지 않은 국가입니다.");
        return true;
    }
    if (!isupdating && index !== -1) {
        setModal("이미 존재하는 국가입니다.");
        return true;
    }
};

const formOnChangeHandler = (e) => {
    const { name, value } = e.target;
    setForm({ ...form, [name]: value });
};
const formAddHandler = (e) => {
    e.preventDefault();
    if (formCheck()) return null;
    setCountries([...countries, { ...form }]);
    setForm(INITIAL_STATE);
};
const formUpdateHandler = (e) => {
    e.preventDefault();
    if (formCheck(true)) return null;
    let newArray = [...countries].map((item) => {
        return item.country === form.country ? form : item;
    });
    setCountries(newArray);
    setForm(INITIAL_STATE);
};
const formDeleteHandler = (e) => {
    const { id } = e.target;
    const newArray = [...countries].filter((el) => el.country != id);
    setCountries(newArray);
};
return (
    <>
        <div className="wrapper">
            <Title />
            <InputForm
                onChange={formOnChangeHandler}
                addHandler={formAddHandler}
                updateHandler={formUpdateHandler}
                value={form}
            />
            {countries.length ? (
                <MedalListBox onClick={formDeleteHandler} countries={countries} />
            ) : (
                <p className="blankMedalList">아직 추가된 국가가 없습니다. 국가를 추가해주세요!</p>
            )}
        </div>
        <Modal elRef={modalRef} alertText={alertText} />
    </>
);

기존에 작성한 코드들이 있어 객체의 값을 바꾸거나 객체를 만들어 추가해주는 방식으로 바꾸면 돼서 쉽게 진행했다.

 

const InputForm = ({ onChange, addHandler, updateHandler, value }) => {
    return (
        <form className={style.inputGroup}>
            {FORM_LIST_INPUT.map((item) => {
                return (
                    <InputFormItem
                        onChange={onChange}
                        name={item.name}
                        label={item.label}
                        type={item.type}
                        key={item.name}
                        value={value}
                        placeholder={item.placeholder}
                    />
                );
            })}
            <Button name="mainBtn" onClick={addHandler}>
                국가추가
            </Button>
            <Button name="mainBtn" onClick={updateHandler}>
                업데이트
            </Button>
        </form>
    );
};

export default InputForm;
import Input from "../common/Input";
import style from "./InputFormItem.module.css";

const InputFormItem = ({ onChange, name, type, label, value, ...rest }) => {
    return (
        <div className={style.inputBoxDiv}>
            <label htmlFor={name} className={style.inputTitleStyle}>
                {label}
            </label>
            <Input type={type} id={name} name={name} onChange={onChange} value={value[name]} {...rest} />
        </div>
    );
};

export default InputFormItem;

이전에 국가명과 메달들은 나눠놓았는데 type을 가진 상수를 미리 선언해둬 그럴 필요가 없어졌다.

상수에 선언된 type에따라 type을 지정하고 ...rest에 placeholder가 담겨 국가명에만 적용되도록 설정했다.

 

import { useState, useEffect, useRef } from "react";
import useDidMountedEffect from "./useDidMountedEffect";
import { INITIAL_STATE } from "../constant";

const useCountry = () => {
    const modalRef = useRef();
    const [alertText, setAlertText] = useState("");
    const [countries, setCountries] = useState([]);
    const [form, setForm] = useState(INITIAL_STATE);

    const setModal = (text) => {
        setAlertText(text);
        modalRef.current.showModal();
    };

    const formCheck = (isupdating = false) => {
        const trimmedName = form.country.trim();
        if (!trimmedName) {
            setModal("국가명을 입력해주세요");
            return true;
        }
        if (form.gold < 0 || form.silver < 0 || form.bronze < 0) {
            setModal("메달수는 0이상만 입력가능합니다.");
            return true;
        }
        const index = countries.findIndex((el) => el.country === form.country);
        if (isupdating && index === -1) {
            setModal("등록되지 않은 국가입니다.");
            return true;
        }
        if (!isupdating && index !== -1) {
            setModal("이미 존재하는 국가입니다.");
            return true;
        }
    };

    const formOnChangeHandler = (e) => {
        const { name, value } = e.target;
        setForm({ ...form, [name]: value });
    };
    const formAddHandler = (e) => {
        e.preventDefault();
        if (formCheck()) return null;
        setCountries([...countries, { ...form }]);
        setForm(INITIAL_STATE);
    };
    const formUpdateHandler = (e) => {
        e.preventDefault();
        if (formCheck(true)) return null;
        let newArray = [...countries].map((item) => {
            return item.country === form.country ? form : item;
        });
        setCountries(newArray);
        setForm(INITIAL_STATE);
    };
    const formDeleteHandler = (e) => {
        const { id } = e.target;
        const newArray = [...countries].filter((el) => el.country != id);
        setCountries(newArray);
    };

    useEffect(() => {
        if (localStorage.getItem("countries")) {
            const localCountries = JSON.parse(localStorage.getItem("countries"));
            setCountries(localCountries);
        }
    }, []);
    const setLocalStorage = () => {
        localStorage.setItem("countries", JSON.stringify(countries));
    };
    useDidMountedEffect(setLocalStorage, countries);

    return {
        modalRef,
        alertText,
        setAlertText,
        countries,
        setCountries,
        form,
        setForm,
        setModal,
        formCheck,
        formOnChangeHandler,
        formAddHandler,
        formUpdateHandler,
        formDeleteHandler,
    };
};

export default useCountry;

마지막으로 state선언, 각종 작업에 관한 함수를 커스텀훅으로 만들어 필요한 컴포넌트에서 가져와 사용하도록 하였다.

import Title from "./container/Title.jsx";
import MedalListBox from "./container/MedalListBox.jsx";
import "./CountryContainer.css";
import Modal from "./container/Modal.jsx";
import InputForm from "./container/InputForm.jsx";
import useCountry from "../hooks/useCountry.jsx";

const CountryContainer = () => {
    const {
        modalRef,
        alertText,
        countries,
        form,
        formOnChangeHandler,
        formAddHandler,
        formUpdateHandler,
        formDeleteHandler,
    } = useCountry();

    return (
        <>
            <div className="wrapper">
                <Title />
                <InputForm
                    onChange={formOnChangeHandler}
                    addHandler={formAddHandler}
                    updateHandler={formUpdateHandler}
                    value={form}
                />
                {countries.length ? (
                    <MedalListBox onClick={formDeleteHandler} countries={countries} />
                ) : (
                    <p className="blankMedalList">아직 추가된 국가가 없습니다. 국가를 추가해주세요!</p>
                )}
            </div>
            <Modal elRef={modalRef} alertText={alertText} />
        </>
    );
};

export default CountryContainer;

커스텀훅으로 빼놓아 CountryContainer가 어떤 컴포넌트인지 조금 더 직관적으로 볼 수 있게 된 것 같다.