[React] props와 useState로 만드는 간단한 장바구니 기능 / useState란?

J.A.Y·2024년 3월 14일
0

javaScript

목록 보기
17/21

들어가기 전 : useState 간단한 정리

💡useState란?
React에서는 보통 원본을 저장하고, 이 원본을 유지한 상태로 값을 업데이트합니다. 다음 포스터에 다룰 예정이지만, 렌더링 시 원본과 비교하여 바뀐 부분만 업데이트 시켜주는 React의 장점을 활용하기 위해서입니다. 그래서 원래는 'state'라고 하여 상태를 저장할 수 있도록 만들어진 것이 있습니다. State의 값이 변경되면 React는 해당 컴포넌트를 리렌더링하여 변화된 부분을 보여줍니다.

원래 'state'는 클라스형 컴포넌트에서만 사용이 가능했습니다. 하지만 함수형 컴포넌트를 많이 사용하게 되면서 함수형 컴포넌트에서도 사용할 수 있도록 등장한 것이 'useState Hook'입니다.

'State'는 객체로 또는 객체가 여러개일 경우엔 객체 배열로 값을 저장하지만, useState는 아래와 같은 형식을 준수해서 작성해줘야합니다.

//useState 훅을 사용하려면 import를 해줘야합니다.
import { useState } from "react";

function Counter() {
    // [원본, set원본명] = useState(값)
  	// useState() 안의 값은 문자도 되고 숫자나, 배열, 객체, blooean 등등 다양하게 작성이 가능합니다.
	const [number, setNumber] = useState(0);

	function increase() {
      	// setNumber(원본을 어떻게 변경할 것인지 로직 작성)
		setNumber(number + 1);
	}

	function decrease() {
		setNumber(number - 1);
	}

	return (
		<div>
      		<p>Number : {number}</p>
			<button onClick={increase}>minus</button>
			<button onClick={decrease}>plus</button>
		</div>
	);
}

🛒장바구니

🔗github 바로가기
🔗 coffe-basket.netlify

Props와 useState를 익히기 위해 음료를 선택하면 해당 음료가 선택되었음을 표시해주는 간단한 장바구니 기능을 만들어보았습니다.

장바구니 기능을 위해 사용한 컴포넌트
1. Table (부모)
2. Thead (자식)
3. Tbody (자식)

1. Thead, Tbody 컴포넌트 (자식)

Table 컴포넌트에 Thead와, Tbody를 각각 불러와 사용하기 위해 우선 Thead, 그리고 Tbody 순서로 컴포넌트를 만들어주었습니다.

- Thead 컴포넌트:

const Thead = ({th1, th2, th3, th4}) => {
    return(
        <thead className="Thead">
            <tr>
                <th>{th1}</th>
                <th>{th2}</th>
                <th>{th3}</th>
                <th>{th4}</th>
            </tr>
        </thead>
    )
}

- Tbody 컴포넌트:

부모 컴포넌트인 Table에서 여러 Tbody를 사용할 때, Tbody의 button을 클릭할 때 클릭한 버튼만 "선택"에서 "삭제로, "삭제"에서 "선택"으로 변경되도록 해주기 위해 handleClick이라는 함수에 onSelect에 boolean값을 담아 Tbody 보내도록 설계했습니다.

그리고, Tbody의 name(=음료명) 또한 onSelect에 포함시켜 선택한 Tbody의 name만이 반환될 수 있도록 설계했습니다.

const Tbody = ({imgURL, name, cost, onSelect}) => {
    const [isSelected, setSelected] = useState(false);
    const handleClick = () => {
        setSelected(!isSelected);
        onSelect(name, !isSelected);
    }
    return(
        <tbody className="Tbody">
            <tr>
                <td><img src={imgURL} /></td>
                <td>{name}</td>
                <td>{cost}</td>
                <td><button className="buttTxt" onClick={handleClick} >{isSelected ? "삭제" : "선택"}</button></td>
            </tr>
        </tbody>
    )
}

2. Table 컴포넌트 (부모)

그런 뒤, 두 자식 컴포넌트를 Table 컴포넌트에 import하고, 각각의 Tbody의 버튼 클릭 시 boolean값을 Tbody에서 넘겨 받아온 후, 이 boolean값을 이용해 선택한 음료명을 테이블 상단에 뜨는 안내 문구를 구현해보았습니다.

선택 > 삭제", "삭제 > 선택" 은 버튼을 클릭할 때마다 Tbody의 button 안에 {isSelected ? "삭제" : "선택"} 적어놓은 삼항연산자가 작동하면서 자동으로 바뀌게 됩니다.

const Table = () => {
    const [announce, setAnnounce] = useState("원하는 음료를 골라주세요.");
    const [selectedNames, setSelectedNames] = useState([]);
  
    let updatedNames = "";
    const handleSelect = (name, isSelected) => {
        //when clicked the button, isSelected data is sent after reverse its value in Tbody.
        if(isSelected) {
            // developed(1): if has, show the 'number' how many menu does a customer has selected same one;
            // developed(2): add animation => if add, bascket size get bigger -> normal during 0.4s ('delete' as well)
            if(!selectedNames.includes(name)){
                selectedNames.push(name);
            }
            updatedNames = selectedNames.join(", ");
        } else {
            const deleteNames = selectedNames.filter((n) => n !== name);
            updatedNames = deleteNames.join(", ");
        }
      updatedNames === "" ? setAnnounce('원하는 음료를 골라주세요.'): setAnnounce(`${updatedNames}를 선택하셨습니다.`);
    }

    return(
        <div className="MenuBox">
            <h1>메뉴판</h1>
            <h2>{announce}</h2>
            <table className="Table table1">
                <Thead th1='음료' th2='음료명' th3='가격' th4='선택'/>
                <Tbody imgURL={todayCoffee} name="Today's Coffee" cost="4000" onSelect={handleSelect} />
                <Tbody imgURL={americano} name="Americano" cost="4000" onSelect={handleSelect} />
                <Tbody imgURL={latte} name="latte" cost="4000" onSelect={handleSelect} />
                <Tbody imgURL={vanillaLatte} name="Vanilla Latte" cost="4000" onSelect={handleSelect} />
                <Tbody imgURL={cappuccino} name="Cappuccino" cost="4000" onSelect={handleSelect} />
                <Tbody imgURL={espressoFrappuccino} name="Espresso Frappuccino" cost="4000" onSelect={handleSelect} />
            </table>
        </div>
    )
}

🚨developed(n) 는 제가 추후 발전시키고 싶은 부분을 써 놓은 것인데, 영어 실력을 늘리기 위해 가끔 재미로 영어 주석을 작성하고 있습니다.

1.❗오류 발생❗

오류 1 : 두번째로 삭제한 음료부터 정상적으로 삭제되지 않고, 이전에 삭제한 음료명으로 바뀜

오류 2 : 여러 개 삭제 후 선택을 누르면, 이전에 삭제됐던 음료명들이 한꺼번에 추가됨

이로 인해 선택하지 않은 음료임에도 선택되었다는 안내 문구가 띄워지는 기이한 상황이 벌어지고 있습니다...

2. 오류 원인

이런 오류가 생긴 이유를 알아보니, 업데이트 된 내용이 selectedNames에 반영이 안 됐거나 selectedNames에 직접 값을 변경했기 때문이었습니다.

제 코드를 다시 보니 정말로 setSelectedNames를 전혀 사용하고 있지 않고 있었습니다. 그래서 setSelectNames를 통해 selectNames값을 업데이트해주고, 업데이트된 내용이 setAnnounce()안에 정상적으로 반환될 수 있도록 useState에서 제공하는 setter를 이용해서 수정해봤습니다.

💡setter?

setter는 React의 useState훅을 사용할 때 반환되는 두 번째 요소로, 원본 상태(state)를 훼손시키지 않고 값을 변경할 때 사용하는 '상태 변경 함수'입니다.

const [count, setCount] = useState(0);

여기서 setter 함수는 setCount 함수입니다.

상태(state)를 이전 상태를 기준으로 변경하고자 한다면 함수를 전달해서 이전 상태를 사용할 수 있습니다.

setCount(prevCount => prevCount + 1);

3. 해결

const [announce, setAnnounce] = useState("원하는 음료를 골라주세요.");
const [selectedNames, setSelectedNames] = useState([]);
    
const handleSelect = (name, isSelected) => {
        if(isSelected) {
            // setter이라는 콜백함수 사용하기
            setSelectedNames(prevSelectedNames => {
                //빈 값이면 추가
                if (prevSelectedNames.length < 1) {
                    return [name];
                }
                // 중복이 아니면 추가
                if(!prevSelectedNames.includes(name)) {
                    return [...prevSelectedNames, name];
                }
                //둘 다 아니면 그냥 반환
                return prevSelectedNames;
            })
        } else {
            setSelectedNames(prevSelectedNames => prevSelectedNames.filter((n) => n !== name));
        }
        setSelectedNames(prevSelectedNames => {
            updatedNames = prevSelectedNames.join(", ");
            setAnnounce(updatedNames === "" ? '원하는 음료를 골라주세요.' : `${updatedNames}를 선택하셨습니다.`);
            return prevSelectedNames;
        })
}

이렇게 수정하고 재실행한 결과, 오류 없이 선택한 음료명이 잘 추가되고 삭제되었습니다.


새롭게 알게 된 것: setter

아래 콘솔창 캡처 이미지를 보면 제가 setSelectedNames(prevSelectedNames => {}) 아래에 console.log()을 넣어 실행했을 때, 콘솔 내용이 먼저 출력되고, 그 이후에 setter 안의 콘솔 내용이 출력된 것을 확인할 수 있습니다.

  • 작성 코드:
        setSelectedNames(prevSelectedNames => {
            console.log("문자열로 변환하는 단계", prevSelectedNames.join(", "))
            updatedNames = prevSelectedNames.join(", ");
            return prevSelectedNames;
        });
        
        console.log("업데이트 문구", updatedNames, "문구 전", selectedNames)
        setAnnounce(updatedNames === "" ? '원하는 음료를 골라주세요.' : `${updatedNames}를 선택하셨습니다.`);

이를 보고 위에서 updatedNames = prevSelectedNames.join(", ")한 것이 setAnnounce(updatedNames === "" ? '원하는 음료를 골라주세요.' : ${updatedNames}를 선택하셨습니다.)에 제대로 반환되지 않았음을 발견하고 setSelectedNames 함수안으로 이동시켜준 것이었습니다.
(원래는 비동기 처리를 해줘야하는 줄 알고 async, await을 사용해 코드를 변경해주기도 했었으나 오류가 발생하여 위의 방법으로 수정한 것입니다.)

이를 통해 알게 된 사실은 다음 두 가지입니다.

  1. useState 내부에는 async와 awiat가 적용되지 않는다는 것
  2. useState 내부는 순차적으로 실행되지만, 자바스크립 전체 코드는 비동기로 실행되므로 결국, useState가 여러개라면 비동기 처리를 해줘야 한다는 것

setter 사용 시 JS의 비동기 부분을 해결할 수 있는 더 쉬운 방법

setAnnounce(updatedNames === "" ? '원하는 음료를 골라주세요.' : ${updatedNames}를 선택하셨습니다.), 즉 useState로 announce를 처리해주던 부분을 없애고 아래처럼 아예 handSelect()함수 밖에서 announce라는 변수에 값을 재할당하는 코드를 작성하면 handSelect()가 완전히 끝난 뒤 실행됩니다.

const announce = selectedNames.length === 0 ? "원하는 음료를 골라주세요." : `${selectedNames.join(", ")}를 선택하셨습니다.`;  
profile
Done is better than perfect🏃‍♀️

0개의 댓글