티스토리 뷰

728x90
반응형
21.09.03 오탈자 수정

 

1. 더보기 버튼 클릭 시, 버튼의 상태 공유 문제

남자 상품에서 더보기 버튼 클릭하고 나열할 항목이 없으면, 위의 사진과 같이 더보기 버튼이 비활성화가 된다. 비활성화가 된 후 여자 상품 카테고리로 갔을 때, 나열할 여자 상품들이 있지만 위와 같이 비활성화가 되는 문제가 있었다. 해당 코드는 아래와 같다.

(ShoesLIst.js 변경전 코드)
(...)
return(
        <>
            <Navigator></Navigator>
            <div className="container">
                <div className="row">
                    {props.num === 1 ? <Woman></Woman> : <Man></Man>}
                </div>
                <Button
                    type="primary"
                    style={{ margin: '4rem' }}
                    onClick={() => {
                        fetchData(props.num);
                    }}>
                    더보기
                </Button>
            </div>
        </>
    );
};

이를 해결하기 위해서 버튼에 대한 상태를 남자, 여자 따로 가지게 했다. props.num===1에 따라 상품을 보여주는 것처럼 버튼도 아래와 같이 UI 컴포넌트로 따로 만들어서 보여줬다. 그리고 남자, 여자 카테고리에서의 버튼 활성화 상태를 props로 전달해줬다. 아래와 같이 바꾼 뒤, 정상적으로 카테고리별로 버튼이 동작되는 것을 확인할 수 있었다.

(ShoesList.js 변경후 코드)
(...)
let [btndisable, setBtnDisable] = useState(false); //상품의 개수가 넘어가면 남자카테고리 더보기 버튼 비활성화
let [wbtndisable, setWBtnDisable] = useState(false); //상품의 개수가 넘어가면 여자 카테고리 더보기 버튼 비활성화

(...)
const ButtonUI = (props) => {
        console.log('더보기 버튼 클릭', props); // shoes 데이터만큼 반복된다.
        return (
            <Button
                disabled={props.whosebtn}
                type="primary"
                style={{ margin: '4rem' }}
                onClick={() => {
                    fetchData(props.all);
                }}>
                더보기
            </Button>
        );
    };
(...)
return (
        <>
            <div className="container">
                <div className="row">
                    {props.num === 1 ? (
                        <Woman shoes={props}></Woman>
                    ) : (
                        <Man shoes={props}></Man>
                    )}
                </div>
                {props.num === 1 ? (
                    <ButtonUI all={props} whosebtn={wbtndisable}></ButtonUI>
                ) : (
                    <ButtonUI all={props} whosebtn={btndisable}></ButtonUI>
                )}
            </div>
        </>
    );
};

 

2. 상품 클릭시 상세 정보 페이지로 이동하는 기능

이제 상품을 클릭했을 때, 상품정보를 나타내는 페이지를 표시해야 한다. 처음에 생각이 난 방법은 총 3가지이다.

  1. history를 이용하기. 하지만 이땐 Link와 정확히 무슨 차이가 있는지 알지 못했다.
  2. 상품을 클릭했을때 history.push("/man/0") 이런식으로 직접 보낼 수 있지 않을까? 하지만 굳이 현 페이지에서 Navbar가 있었기에 굳이 사용할 필요는 없었다.
  3. Link, Route를 사용하기.

내가 헷갈렸던 부분이 있다. App.js에서 라우트 설정을 했지만, 상품을 클릭했을 때 라우트를 따로 설정해야 된다고 생각했다. 하지만 Route는 사용자가 링크를 접속했을 때, 해당 컴포넌트를 표시해주는 역할을 한다. 그런데 클릭이벤트 시 실행 함수 안에 <Route>를 넣어버렸다..😂 이것도 잘못됐었지만, <ShoesList>는 HTML태그가 아닌 컴포넌트이기 때문에 클릭이벤트를 설정할 수 없었는데 클릭이벤트로 를 넣어버렸다.

따라서 생각이 난 것은 Link밖에 없었다. 아래와 같이 ShoesItem 자체를 감싸줬다. 그리고 props 값들을 이용해 url의 이동을 만들었다. 아래와 같이 각각의 상품을 클릭했을 때, 각각의 상품에 상세 정보창을 호출하도록 만들었다. 그러면 해당 상품창으로 이동할 수 있다.

(./components/shoesItem.js)
(...)
function ShoesItem(props) {
    let src = '/' + props.sex + '/' + props.shoes.id;
    return (
        <div className="col-md-4">
            <Link to={src}>
                <img src={props.shoes.imageUrl} width="100%"></img>
                <h4>{props.shoes.title}</h4>
                <h5>₩ {props.shoes.price}</h5>
            </Link>
        </div>
    );
}
(...)

 

위와 같이 정상적으로 상세 페이지 작동이 되는 것을 확인할 수 있었다. 하지만 또 다른 문제가 발생했다.... 3번에서 해결해보자.

 

(21.08.10)

2.1. Link를 history.push로 수정 😂

Link와 history.push의 차이점이 뭘까?

stackoverflow에서 비슷한 질문을 확인할 수 있었다. 버튼이나 링크처럼 클릭할 필요가 없는 것도 프로그램적으로 url을 바꿀 수 있게 해 준다. 즉, html태그에서 버튼 없이도 onClick 이벤트로 보내줄 수 있다. 큰 차이는 없었지만, 코드상에서는 div 태그에 클릭이벤트로 history를 쓰는 것이 더 좋아 보인다. 그래서 아래와 같이 수정했다.

(ShoesItem.js)
(...)
let history = useHistory();
let src =
    props.sex === 'womanshoes'
? '/womanshoes/' + props.shoes.id
: '/manshoes/' + props.shoes.id;
return (
        <div
            className="col-md-4"
            onClick={() => {
                console.log({ src });
                history.push(src);
            }}>
            <img src={props.shoes.imageUrl} width="100%"></img>
            <h4>{props.shoes.title}</h4>
            <h5>₩ {props.shoes.price}</h5>
        </div>
    );

 

 

3. 더보기 버튼 클릭후, 추가된 상품들 클릭 시 오류

위와 같이 상품들이 정상적으로 나오지만, 더보기 버튼을 클릭하고 나서 추가된 상품들을 클릭했을 때는 오류가 생겼다. 계속 undefinedShoesItem으로 전달되서 오류가 나는 것을 확인했다.

이유를 살펴보니 ShoesItem으로 전달되는 정보는 기존의 App.js에서 가지고 있는 신발 상품 데이터들이다. 하지만 더보기 버튼으로 추가된 이후에, App.js에서는 추가된 상품의 정보가 업데이트되지 않아 상세페이지로 갈 때 정보가 없어서 오류가 났던 것이다.

생각해보면 DB에서는 이미 데이터 집합을 다 가지고 있고, 개수에 따라 사용자에게 적절한 개수를 보여줘야 한다. 하지만 나는 기존의 데이터를 보여준 다음 더보기 버튼을 눌렀을 때, json파일의 데이터를 불러오는 식으로 구성했다.

아래 코드를 보면 데이터가 추가되는 경우엔 아래와 같이 남자 상품 데이터, 여자 상품 데이터를 클릭했을 때, 각각의 Route에 보내줬다. 서로의 State를 가지고 있어야 한다. 코드를 짜다 보니 계속 여자 카테고리와 남자 카테고리를 넘나들면서 오류가 생긴다. 결국 둘 다 서로의 State를 가지고 있어야 내 생각대로 UI 구성이 가능했다. 그래서 App.js에서의 State 변경 함수를 props로 넘겨줬다.

또한 ShoesList에선 항상 남자, 여자 데이터를 가지고 있어야하므로, props로 데이터들을 모두 넘겨줬다.

(App.js 변경 후 코드)
(...)
    return (
            <Switch>
                <Route exact path="/">
                    <MainPage></MainPage>
                </Route>
                <Route path="/manshoes/:id">
                    <About shoes={shoes} wshoes={wshoes} num={0}></About>
                </Route>
                <Route path="/womanshoes/:id">
                    <About shoes={shoes} wshoes={wshoes} num={1}></About>
                </Route>
                <Route exact path="/manshoes">
                    <ShoesList
                        shoes={shoes}
                        wshoes={wshoes}
                        setShoes={setShoes} //State 변경 함수 전달
                        num={0}></ShoesList>
                </Route>
                <Route exact path="/womanshoes">
                    <ShoesList
                        shoes={shoes}
                        wshoes={wshoes}
                        setWShoes={setWShoes} //State 변경 함수 전달
                        num={1}></ShoesList>
                </Route>
                <Route path="/cart">
                    <Cart></Cart>
                </Route>
                <Route path="/description">
                    <Astronaut></Astronaut>
                </Route>
            </Switch>
        </div>
    );
};

:id를 붙여 /manshoes/ 이후 id를 바인딩시켜 추가적으로 생성되는 사이트들을 전부 URL 파라미터를 통해 해결했다. Route를 통해 보여지게되는 <About/> 컴포넌트에서는 useParams라는 훅을 사용해, :id 값을 컴포넌트내에서 활용할 수 있게 해 준다.

useHistory, useParams, useLocation 설명글

위와 같이 변경한 후엔 추가된 상품을 클릭해도 상세페이지로 이동하는 것을 확인할 수 있었다.

 

4. 장바구니에 담기

이제 더보기 버튼을 눌러서 상품이 추가됐을 때, 상품들을 눌러도 상세페이지로 이동할 수 있다. 이 상세페이지에서 장바구니에 담기 버튼을 누르면 Cart 페이지로 상품이 이동한다. 아래와 같이 추가되는 것을 확인할 수 있다. 항목을 삭제하거나 추가를 하면 `redux`의 데이터를 변경해준다. 이 부분은 서버에서 상태를 저장하면서 관리하는 것 변경할 계획이다. 현재는 JS파일 안에 정적으로 선언되어있기 때문에 새로고침을 누르면 데이터의 변경이 초기 설정값으로 돌아간다.

결제금액도 수량을 바꿀 때마다 실시간으로 바뀌도록 설정했다.

 

5. Redux에 대해서

props 쓰기 귀찮아서 쓰는 Redux. 왜냐하면 데이터 하나 보내려고 App.js 상단에서부터 3개의 컴포넌트 거쳐서 About.js까지 전달해야 했다. props를 사용하지 않고 전역적으로 State를 관리해 어떤 컴포넌트에서도 호출할 수 있기에 사용한다. 

redux를 서드 파티 라이브러리라고 부른다.

https://eastflag.co.kr/react/scoreboard-advanced/react-redux-tutorial/

  1. props 쓰기 싫어서. 모든 컴포넌트들이 같은 State값을 공유하게 만들 수 있다.
  2. state 데이터 관리 가능.

아래와 같이 라이브러리를 설치한다.

yarn add redux react-redux
npm install redux react-redux
(index.js)
import { Provider } from 'react-redux';
import { createStore } from 'redux';

let store = createStore(() => {
    return [{ id: 0, name: '신발', remain: 2 }];
});
ReactDOM.render(
    <React.StrictMode>
        <BrowserRouter>
            <Provider> //App을 감싸준다.
                <App />
            </Provider>
        </BrowserRouter>
    </React.StrictMode>,
    document.getElementById('root')
);
(Cart.js)
function 함수명(state) {
    //redux store안의 데이터를 가져와서 props로 변환해주는 함수
    return {
        상품명: state.name,
        state: state,
    };
}
export default connect(함수명)(Cart);
// export default Cart;

이처럼 깊은 하위 컴포넌트들도 props 여러 번 전송 없이 state를 직접 갖다 쓸 수 있다.

그러면 Data, Data2도 redux로 쓸 수 있나?

state란 변수 안에 저장은 된다. 하지만 map 함수를 쓸 때 해당 인자를 선택할 때 문제를 조금 고민해봐야 한다.

let store = createStore(() => {
    return [
        { Data },
        { Data2 },
        [
            { id: 0, name: '멋진신발', remain: 2 },
            { id: 1, name: '나이키신발', remain: 3 },
        ],
    ];
});

이렇게 변경해줄 수 있다.

state 데이터 관리 가능

redux에선 state데이터의 수정 방법을 미리 정의한다. -> reducer

reducer는 그냥 수정된 state를 뱉는 함수.

let reduxData = [
    { Data },
    { Data2 },
    [
        { id: 0, name: '멋진신발', remain: 2 },
        { id: 1, name: '나이키신발', remain: 3 },
    ],
];
const reducer = (state = reduxData, action) => {
    if (){
        return 수정된state
        }
    return state;
};

default parameter 문법

state데이터 관리가 용이하다. 대규모 프로젝트에선 필요하다. 지금 내 사이트 같은 경우에는 State가 많지 않기 때문에 redux가 필요하진 않다.

let reduxData = [
    { id: 0, name: '멋진신발', remain: 2 },
    { id: 1, name: '나이키신발', remain: 3 },
];
const reducer = (state = reduxData, action) => {
    if (action.type === '수량증가') {
        let copy = [...state];
        console.log(copy);
        copy[0].remain++;
        return copy;
    } else if (action.type === '수량감소') {
        let copy = [...state];
        if (copy[0].remain < 1) copy[0].remain = 0;
        else copy[0].remain--;
        return copy;
    } else {
        return state;
    }
};
let store = createStore(reducer);

여러 개의 reducer

여러개의 reducer를 활용해도 합칠 수 있다.

import { Provider } from 'react-redux';
import { combineReducers, createStore } from 'redux';
let btn_alert = true;

const alert = (state = btn_alert, action) => {
    if (action.type === '알림닫기') {
        return !alert;
    }
    return state;
};

let reduxData = [
    { id: 0, name: '조던신발', remain: 2 },
    { id: 1, name: '나이키신발', remain: 3 },
];
const remainReducer = (state = reduxData, action) => {
    if (action.type === '수량증가') {
        let copy = [...state];
        copy[0].remain++;
        return copy;
    } else if (action.type === '수량감소') {
        let copy = [...state];
        if (copy[0].remain < 1) copy[0].remain = 0;
        else copy[0].remain--;
        return copy;
    } else {
        return state;
    }
};

let store = createStore(combineReducers({ remainReducer, alert }));

<Provider store={store}>

redux로 데이터를 실어 보낼 수도 있다.

<button
    onClick={() => {
        props.dispatch({
            type: '수량감소',
            payload: { name: 'kim' },
        });
    }}>
    -
</button>
  • 보낸 자료는 액션 파라미터에 저장된다.
  • 액션.payload 로 꺼낼 수 있다.
  • 장바구니 기능으로 활용
About.js에 추가
<button
    className="btn btn-danger"
    onClick={() => {
        props.dispatch({ type: '항목추가' });
    }}>
    장바구니
</button>

const redux = (state) => {
    console.log(state);
    return {
        state: state,
    };
};

export default connect(redux)(About);
let reduxData = [
    { id: 0, name: '멋진신발', remain: 2 },
    { id: 1, name: '나이키신발', remain: 3 },
];
const remainReducer = (state = reduxData, action) => {
    if (action.type === '수량증가') {
        let copy = [...state];
        copy[0].remain++;
        return copy;
    } else if (action.type === '수량감소') {
        let copy = [...state];
        if (copy[0].remain < 1) copy[0].remain = 0;
        else copy[0].remain--;
        return copy;
    } else if (action.type === '항목추가') {
        let copy = [...state];
        copy.push(action.payload);
        return copy;
    } else {
        return state;
    }
};

개발환경에서 새로고침 하면 redux도 초기화된다. 데이터를 잃어버림. 개발환경에서 페이지 이동시 강제 새로고침 안되게 하려면 useHistory 훅을 사용한다.

state를 더 쉽게 꺼내 쓰는 방법이 있다. useSelector를 활용

    let state = useSelector(
        (state) =>
            //redux안에 있던 모든 state
            state.remainReducer
    );
    let dispatch = useDispatch();

훅을 이용해서 state 값을 편하게 가져오는 방법이 있다.

정리

현재 내 프로젝트에선 Cart.js 장바구니 기능에서 redux가 사용되고 있다. 확실히 쓰기엔 복잡하지만 state관리가 편리했다. 또한 버튼 기능에 오류가 생겼을 때, props 전달 값을 일일이 확인하는 것이 아닌 reducer 부분만 확인하면 돼서 편리했다. 

 

😃 추후 계획

이제 최적화를 시키는 부분이 필요한 것 같다. console.log()로 데이터를 확인할 때마다 불필요한 렌더링이 일어나는 것을 중간중간 확인할 수 있었다. 하지만 기능 개발을 우선적으로 해 신경을 쓰지 못했다. 이제 남은 기능 개발은 Node.js와 연동하는 것과 간단한 결제 기능? 최적화만 같이 해준다면, 얼핏 보기에는 그럴싸한 쇼핑몰을 완성시킬 수 있을 거 같다.

728x90
반응형

'Project' 카테고리의 다른 글

[React] Astronaut-shop 삽질 개발일지1  (0) 2021.08.08
[Toy프로젝트 ] Astronaut 개발일지  (0) 2021.08.04
댓글