본문 바로가기
  • 삽질하는 자의 블로그
메인-프로젝트/React - Do-Health 프로젝트

4. 기본 UI 만들기(4) - [ Redux-toolkit ] 버튼을 누르면, 해당 컴포넌트가 렌더되는 페이지 만들기

by 이게뭐당가 2022. 12. 28.

프로세스 및 워크플로우

[일단 파츠(부위)별 컴포넌트를 전부 만든다.]

    "HowToWorkOut Page" 는
    "Choose Part 컴포넌트" , "Descripion 컴포넌트" 로 구성

    "Choose Part 컴포넌트" 는 기본적으로 "디폴트 Description" 이 같이 렌더
    "Choose Part 컴포넌트" 에서 "해당 부위의 버튼을 누르면"
    " 해당 부위의 Description"이 "렌더된다."

[리덕스]를 이용하여, 리덕스를 이용하여, State 를 만들어서, 
    "Choose Part 컴포넌트"에서, 버튼을 누르면,  "HowToWorkOut Page" 에서, 받아, 다른 것을 렌더하는 형식으로 하자

[ Descripion 이 바뀔때는, React-Transition-Group 을 사용하여]
    마치 슬라이드 되듯 넘어가는 화면을 구축해보자


< 워크 플로우 >

    [HowToWorkOut Page]
            |
            |---- choose-parts.tsx          // 기본
            |
            |---- 0. default-description.tsx   // 기본 (버튼 선택 전까지)
            |
            |
            |---- 1.chest-part-description  // 버튼선택시
            |       * (0.all-parts-description.module.css) // css동일
            |           |
            |           |
            |           ---- DesCard.tsx (동적 카드)
            |       
            |
            |---- 2.back-part-description   // 버튼선택시
            |       * (0.all-parts-description.module.css)  // css동일
                        ...

 

 

 

 

 

변환동작 구현 [ 총 코드 정리 ]

 

1. 슬라이스 생성

[1. 리덕스툴킷 슬라이스 "store/choose-part-slice.tsx" ]

    import { createSlice } from "@reduxjs/toolkit";

    export type PartState = {       // 반드시 객체안에, 한번 담아서, 한번에 초기화가 가능하게 만든다.
        descriptionState: {         // State 를 reducer 에서 바로 사용하면 Proxy 가 생기는 오류가 발생한다.
            onDefaultPart: boolean;
            onChestPart: boolean;
                ...
            onLegsPart: boolean;
            onGlutesPart: boolean;
        };
    };

    const initialState: PartState = {
        descriptionState: {
            onDefaultPart: true,
            onChestPart: false,
                ...
            onLegsPart: false,
            onGlutesPart: false,
        },
    };

    const ChoosPartsSlice = createSlice({
        name: "choose-part",
        initialState,
        reducers: {
            ChestDescription(state) {
                state.descriptionState = { ...initialState.descriptionState };  // State 를 바로 사용하지 않고, 한번 넣어 사용했기에 가능하다.
                state.descriptionState.onDefaultPart = false;                   // 디폴트는 없애고
                state.descriptionState.onChestPart = true;                      // 각각에 맞는 State 를 업데이트한다.
            },
                    ...
            LegsDescription(state) {
                state.descriptionState = { ...initialState.descriptionState };
                state.descriptionState.onDefaultPart = false;
                state.descriptionState.onLegsPart = true;
            },
            GlutesDescription(state) {
                state.descriptionState = { ...initialState.descriptionState };
                state.descriptionState.onDefaultPart = false;
                state.descriptionState.onGlutesPart = true;
            },
        },
    });

    export const ChooseActions = ChoosPartsSlice.actions;

    export default ChoosPartsSlice.reducer;

** 반드시, initialState 에서는, 한 번은 객체 혹은 배열에 담아서 사용하도록 하자.

   그렇지 않으면 reducer 에서, state 를 바로 사용할 수 없다는 것 때문에, 한번에 초기화를 하지 못한다.

https://dive-into-frontend.tistory.com/127

 

1. dispatch 가 작동하지 않는다면, 혹시 State 를 reducer 함수에 직접 썼는지 확인한다.

리덕스의 reducer 함수 안에서, State 에 바로 접근 하는 것은 불가능하다. const ChoosPartsSlice = createSlice({ name: "choose-part", initialState, reducers: { ChestDescription(state) { state = {...initialState} // state 에 바로접근

dive-into-frontend.tistory.com

** 또한, initialState의 타입을 만들때, Partial 를 사용하지 않도록 한다.

   타입이 정확히 지정되지 않으면 reducer 함수에서 state를 사용하지 못할 수 있다.

 

 

 

 

 

 

 

 

2. 스토어 생성

    import { configureStore } from "@reduxjs/toolkit";
    import choosePartSlice from "./choose-part-slice";

    export const store = configureStore({
        reducer: { choosePart: choosePartSlice },
    });

    export type RootState = ReturnType<typeof store.getState>;
    export type AppDispatch = typeof store.dispatch;

 

 

 

 

 

3. 스토어 등록

    import ReactDOM from "react-dom/client";
    import "./index.css";
    import App from "./App";
    import { BrowserRouter } from "react-router-dom";
    import Layout from "./components/UI/layout/layout";
    import { Provider } from "react-redux";
    import { store } from "./store";

    const root = ReactDOM.createRoot(
    	document.getElementById("root") as HTMLElement
    );
    root.render(
        <Provider store={store}>
            <BrowserRouter>
                <Layout>
                    <App />
                </Layout>
            </BrowserRouter>
        </Provider>
    );

 

 

 

 

 

4. 파트 선택 컴포넌트 생성 [ useDispatch , useCallback ]

[4. 파트 선택 컴포넌트 "components/how-to-workout/choose-parts.tsx"]

    import styles from "./choose-parts.module.css";
    import { useCallback } from "react";
    import { useDispatch } from "react-redux";
    import { ChooseActions } from "../../store/choose-part-slice";

    const ChooseParts = () => {
        const dispatch = useDispatch();                     // dispatch 사용

        const onChestHandler = useCallback(() => {
            dispatch(ChooseActions.ChestDescription());     // useCallback 과 함께, actions, dispatch
        }, [dispatch]);
                    ...

        const onLegsHandler = useCallback(() => {
            dispatch(ChooseActions.LegsDescription());
        }, [dispatch]);
        const onGlutesHandler = useCallback(() => {
            dispatch(ChooseActions.GlutesDescription());
        }, [dispatch]);

        return (
                    ...

            <ul className={styles.select_parts}>
                <li onClick={() => onChestHandler()}>       // 각각의 li 에 리스너 등록
                    <div className={styles.parts_text}>
                        <img src="/2.parts/torso.png" alt="torso" />
                        <p>가슴</p>
                    </div>
                    <div className={styles.parts_img}>
                        <img src="/2.parts/icons8-circled-right-100.png" alt="arrow" />
                    </div>
                </li>
                        ...

                <li onClick={() => onShoulderHandler()}>
                    <div className={styles.parts_text}>
                        <img src="/2.parts/shoulders.png" alt="back" />
                        <p>어깨</p>
                    </div>
                    <div className={styles.parts_img}>
                        <img src="/2.parts/icons8-circled-right-100.png" alt="arrow" />
                    </div>
                </li>
                <li onClick={() => onBicepsHandler()}>
                    <div className={styles.parts_text}>
                        <img src="/2.parts/biceps.png" alt="biceps" />
                        <p>이두</p>
                    </div>
                    <div className={styles.parts_img}>
                        <img src="/2.parts/icons8-circled-right-100.png" alt="arrow" />
                    </div>
                </li>
                        ...

** useDispatch 를 사용하면, 타입스크립트는, useCallback 함수를 같이 사용하라고 조언한다.

   이는, 함수가 새로 생성되는 것을 방지하고, 상속처럼, 메모리의 낭비를 방지할 것이다.

 

 

 

 

 

 

5. Page 에서, State에 따라, 다른 컴포넌트 렌더

[5. Page 에서, State 에 따라 다른 컴포넌트 렌더 "/pages/2.how-to-workout.jsx"]

    import ChooseParts from "../components/2.how-to-workout/choose-parts";
    import DefaultDes from "../components/2.how-to-workout/default-description";
    import ChestPartDes from "../components/2.how-to-workout/1.chest-part-description";
                    ...
    import LegsPartDes from "../components/2.how-to-workout/7.legs-part-description";
    import GlutesPartDes from "../components/2.how-to-workout/8.glutes-part-description";

    import { useSelector } from "react-redux";
    import { RootState } from "../store";

    const HowToWorkOut = () => {
        const chooseDescription = useSelector((state: RootState) => state.choosePart);  // state 를 빼서 사용

        return (
            <div>
                <ChooseParts />
                {chooseDescription.descriptionState.onDefaultPart && <DefaultDes />}
                {chooseDescription.descriptionState.onChestPart && <ChestPartDes />}
                            ...
                {chooseDescription.descriptionState.onLegsPart && <LegsPartDes />}
                {chooseDescription.descriptionState.onGlutesPart && <GlutesPartDes />}
            </div>
        );
    };

    export default HowToWorkOut;

 

 

 

 

6. 렌더되는 Description 컴포넌트 생성

[6. 렌더되는 Descripion "components/how-to-work-out/1.chest-part-description.tsx" ... 등]

    import DesCard from "../UI/descriptionCard/DesCard";        // 카드 아웃소싱
    import styles from "./0.all-parts-description.module.css";

    const ChestPartDes = () => {
        return (
            <div className={styles.main_div}>
                <DesCard				// 아웃소싱한 카드 동적으로 활용
                    name={"벤치프레스"}
                    img={"/2.parts/do-parts/graham-mansfield-rkBkXqlfRRo-unsplash.jpg"}
                    des={[
                        { id: 1, text: "팔꿈치와 명치 사이의 수직 각도" },
                        { id: 2, text: "충분한 허리의 아치각도 유지하기" },
                    ]}
                    warn={[
                        { id: 1, text: "팔꿈치와 명치 사이의 수직 각도" },
                        { id: 2, text: "충분한 허리의 아치각도 유지하기" },
                    ]}
                />
            </div>
        );
    };

    export default ChestPartDes;

 

 

 

 

7. 실제로 화면에 보이는 [ 운동 방법 카드 ]

[ 7. 실제로 화면에 보이는 [운동 방법 카드] "/components/UI/descriptionCard/DesCard.tsx"]

    import styles from "./DesCard.module.css";
    import { AnimationOnScroll } from "react-animation-on-scroll";

    export type Exr = {     // 타입 정하기
        name: string;
        img: string;
        des: { id: number; text: string }[];
        warn: { id: number; text: string }[];
    };

    const DesCard: React.FC<Exr> = (props) => {
        const { name, img, des, warn } = props;
        return (
            <div className={styles.main_card}>
                <div className={styles.title_div}>
                    <p>{name}</p>
                </div>

                {/* Description Part */}

                <div className={styles.main_desc_div}>
                    <ul className={styles.main_desc_text_ul}>
                        {des.map((item) => (
                            <li key={item.id}> {item.text}</li>
                        ))}
                    </ul>
                    <div className={styles.main_desc_img_div}>
                        <img src={img} alt={"img"}></img>
                    </div>
                </div>

                {/* Warning Part */}

                <div className={styles.main_warning_div}>
                    <p> 유의사항</p>
                    <ul className={styles.main_warning_lists}>
                        {warn.map((item) => (
                            <AnimationOnScroll animateIn="animate__bounceIn" key={item.id}>     // 애니메이션 적용
                                <li key={item.id}> {item.text}</li>
                            </AnimationOnScroll>
                        ))}
                    </ul>
                </div>
            </div>
        );
    };

    export default DesCard;

 

댓글