
μ΄μ κ²°μ νμ΄μ§λ₯Ό μμ±ν΄λ³΄λ €κ³ νλ€. κ·Έλ¬λ©΄ μμ μ₯λ°κ΅¬λμμ 체ν¬ν μνλ°μ΄ν°λ₯Ό κ·Έλλ‘ κ²°μ νμ΄μ§λ‘ κ°μ ΈμμΌνλ€.
β λ°λΌμ, recoilμ ν΅ν΄ μνκ΄λ¦¬λ₯Ό ν΄λ³΄λλ‘ νμ!
recoilμ μνκ΄λ¦¬κ° νμν κ³³μ 체ν¬λ μνμ΄ κ²°μ μμ μνμΌλ‘ μΆλ ₯λμΌνλ©°, κ²°μ νμ΄μ§μλ μΆλ ₯λμ΄μΌνλ€.
β
μ°μ , handleCheckedChangedκ° μ€νλ λ μ¦, μν체ν¬κ° λ λ ν΄λΉ μν λ°μ΄ν°λ₯Ό μ λ¬νμ.
checkedItems : reduceμ 체ν¬λ μνλ°μ΄ν°λ₯Ό λμ ν©μ°ν κ°
useRecoilState : atom νΉμ selectorμ κ°μ μ½κ³ μ°λ €κ³ ν λ μ¬μ©. μνμ λ³κ²½ μ¬νμ λ€μ λ λλ§νκΈ° μν΄ κ΅¬μ± μμλ₯Ό ꡬλ
. (μ°Έκ³ )
useEffect: items, formDataμ λ³κ²½μ λ°λΌ λλλ§
μ 리 : λ°μ΄ν°λ€μ 체ν¬λ μνκ° κ·Έλλ‘ recoil Atom(μν μ μ₯μ?)μ μ λ¬.// components/cart/indext.tsx
const CartList = ({ items }: { items: CartType[] }) => {
const [checkedCartData, setCheckedCartData] = useRecoilState(checkedCartState);
const [formData, setFormData] = useState<FormData>();
...
useEffect(() => {
const checkedItems = checkboxRefs.reduce<CartType[]>((res, ref, i) => {
if (ref.current!.checked) res.push(items[i]);
return res;
}, []);
setCheckedCartData(checkedItems);
}, [items, formData]);
...
};
β Atom κ°μ²΄ λ°°μ΄μ 체ν¬λ λ°μ΄ν° μ λ¬.
Atomμ΄ μ
λ°μ΄νΈλλ©΄ ν΄λΉ Atomμ ꡬλ
νκ³ μλ λͺ¨λ μ»΄ν¬λνΈλ€μ΄ μλ‘μ΄ κ°μΌλ‘ 리λ λλ§.Atomμ ꡬλ
νκ³ μμΌλ©΄ κ·Έ μ»΄ν¬λνΈλ€μ΄ μνλ₯Ό λμΌνκ² κ³΅μ νλ€.// src/recoils/cart.ts
import { atom } from "recoil";
import { CartType } from "../graphql/cart";
export const checkedCartState = atom<CartType[]>({
key: "cartState",
default: [],
});
β λ°μ΄ν° κ°μ Έμ€κΈ°
useRecoilValue(): Recoil μνκ° λ³κ²½λλ κ²½μ° κ΅¬μ± μμλ₯Ό ꡬλ
νμ¬ μ¬λλλ§λμ΄ μ
λ°μ΄νΈλ κ°μ κ°μ Έμ¨λ€.// components/willPay/indext.tsx
const WillPay = (...) => {
const checkedItems = useRecoilValue(checkedCartState);
...
return (
<div className="cart-willpay">
<ul>
{checkedItems.map(({ imageUrl, price, title, amount, id }) => (
<li key={id}>
<ItemData imageUrl={imageUrl} price={price} title={title} />
<p>μλ: {amount}</p>
<p>κΈμ‘: {price * amount}</p>
</li>
))}
</ul>
...
</div>
);
};
β κ²°μ μμ μ°½ μΆλ ₯
// components/cart/index.tsx
const CartList = ({ items }: { items: CartType[] }) => {
const navigate = useNavigate();
const [checkedCartData, setCheckedCartData] = useRecoilState(checkedCartState);
...
const handleSubmit = () => {
if (checkedCartData.length) navigate("/payment");
else alert("κ²°μ ν λμμ΄ μμ΄μ");
};
return (
...
<WillPay submitTitle="κ²°μ μ°½μΌλ‘" handleSubmit={handleSubmit} />
)
}
// components/willPay/indext.tsx
const WillPay = ({
submitTitle,
handleSubmit,
}: {
submitTitle: string;
handleSubmit: (e: SyntheticEvent) => void;
}) => {
...
return (
...
<button onClick={handleSubmit}>{submitTitle}</button
)
}
β
κ²°μ μ°½ μΆλ ₯ μ μμλ νμΈ

체ν¬λ μνμ λνμ¬ λ°μ΄ν°κ° μ μ λ¬λλ κ²μ νμΈνλ€. νμ§λ§ κ²°μ νμ΄μ§λ‘ μ΄λ ν λ€μ μ₯λ°κ΅¬λ νμ΄μ§λ‘ μ΄λν κ²½μ° μ²΄ν¬κ° νλ € 체ν¬νλ λ°μ΄ν°κ° λ λΌκ°λ²λ¦°λ€.
β
λ°λΌμ, μ₯λ°κ΅¬λνμ΄μ§κ° λλλ§λ λ μ₯λ°κ΅¬λμ μν μ€μ 체ν¬λμλ μνμ idμ κ°μ λ°μ΄ν°λ₯Ό μ°Ύμμ checkκ°μ΄ trueκ° λκ² λ§λ λ€.
data-*: data-* μ μ μμ±μ μ¬μ©μ μ§μ λ°μ΄ν° μμ±μ΄λΌλ μμ± ν΄λμ€λ₯Ό νμ±νλ©°, μ΄λ₯Ό ν΅ν΄ μ€ν¬λ¦½νΈλ₯Ό ν΅ν΄ HTMLκ³Ό ν΄λΉ DOM νν κ°μ λ
μ μ 보λ₯Ό κ΅ν κ°λ₯.
dataset: HTMLElement μΈν°νμ΄μ€μ μ½κΈ° μ μ© μμ±μΈ datasetμ μμμ μ¬μ©μ μ μ λ°μ΄ν° μμ±(data-*)μ λν μ½κΈ°/μ°κΈ° μ‘μΈμ€λ₯Ό μ 곡νλ€. κ° data-* μμ±μ λν νλͺ©μ΄ μλ λ¬Έμμ΄ λ§΅(DOMStringMap)μ λ
ΈμΆ.
// comonents/cart/CartItem.tsx
<input ... data-id={id} ... />
// components/cart/index.tsx
useEffect(() => {
checkedCartData.forEach((item) => {
const itemRef = checkboxRefs.find(
(ref) => ref.current!.dataset.id === item.id
);
if (itemRef) itemRef.current!.checked = true;
});
setAllCheckedFromItems();
}, []);
β
setAllCheckedFromItemsλ κ°λ³μ μΌλ‘ μνμ 체ν¬νμ κ²½μ° λμνλ ν¨μ
const setAllCheckedFromItems = () => {
// κ°λ³μμ΄ν
μ νμ
if (!formRef.current) return;
const data = new FormData(formRef.current);
const selectedCount = data.getAll("select-item").length;
const allChecked = selectedCount === items.length;
formRef.current.querySelector<HTMLInputElement>(".select-all")!.checked =
allChecked;
};
β
λ°λλ‘ μ 체μ νμ ν΄λ¦νμ¬ λͺ¨λ 체ν¬λ°μ€κ° 체ν¬λλ κ²½μ°λ₯Ό setItemsCheckedFromAll ν¨μλ‘.
const setItemsCheckedFromAll = (targetInput: HTMLInputElement) => {
const allChecked = targetInput.checked;
checkboxRefs
.filter((inputElem) => {
return !inputElem.current!.disabled;
})
.forEach((inputElem) => {
inputElem.current!.checked = allChecked;
});
};
β
λ°λΌμ, μμ λ§λ handleCheckedboxChanged ν¨μλ λ€μκ³Ό κ°μ΄ μ‘°κΈλ κ°λ¨νκ² μμ±
const handleCheckboxChanged = (e?: SyntheticEvent) => {
if (!formRef.current) return;
const targetInput = e?.target as HTMLInputElement;
if (targetInput && targetInput.classList.contains("select-all")) {
setItemsCheckedFromAll(targetInput);
} else {
setAllCheckedFromItems();
}
const data = new FormData(formRef.current);
setFormData(data);
};
β
μ₯λ°κ΅¬λ λ°μ΄ν° λ° μ²΄ν¬ μ μ§ νμΈ

μ΄μ κ²°μ νμ΄μ§λ₯Ό λ§λ€μ΄λ³΄μ. κ²°μ νμ΄μ§μμ νΉλ³ν λμνλ κΈ°λ₯μ μκ³ , 체ν¬λ μνμ λΆλ¬μ€κ³ , κ²°μ νκΈ°μ λ°λΌ λ°μ΄ν°μ μμ λ° μ μ§λ₯Ό λνλ΄μΌ νλ€.
β 체ν¬λ μν μ¦, κ²°μ μμ μ°½μ μΆλ ₯λ λ°μ΄ν°λ€μ κ°μ Έμ€κΈ°
WillPay μ»΄ν¬λνΈλ₯Ό κ·Έλλ‘ λΆλ¬μ€μ.// components/payment/index.tsx
const Payment = () => {
...
return (
<div>
<WillPay submitTitle="κ²°μ νκΈ°" handleSubmit={showModal} />
...
</div>
);
};
β κ²°μ νκΈ° ν΄λ¦μ λͺ¨λ¬μ°½ μΆλ ₯νκΈ°
WillPay μ»΄ν¬λνΈμ κ²°μ νκΈ° ν΄λ¦μ showModalμ΄ μ€νλμ΄ λͺ¨λ¬μ°½μ΄ μΆλ ₯. // components/payment/index.tsx
const Payment = () => {
const navigate = useNavigate();
const [modalShow, setModalShow] = useState(false);
...
const showModal = () => {
setModalShow(true);
};
const proceed = () => {
...
};
const cancel = () => {
setModalShow(false);
};
return (
<div>
<WillPay submitTitle="κ²°μ νκΈ°" handleSubmit={showModal} />
<PaymentModal show={modalShow} proceed={proceed} cancel={cancel} />
</div>
);
};
Portal: λΆλͺ¨ μ»΄ν¬λνΈμ DOM κ³μΈ΅ ꡬ쑰 λ°κΉ₯μ μλ DOM λ
Έλλ‘ μμμ λ λλ§νλ μ΅κ³ μ λ°©λ²// index.html
...
<div id='modal'><div/>
...
// components/payment/modal.tsx
const ModalPortal = ({ children }: { children: ReactNode }) => {
return createPortal(children, document.getElementById("modal")!);
};
const PaymentModal = ({
show,
proceed,
cancel,
}: {
show: boolean;
proceed: () => void;
cancel: () => void;
}) => {
return show ? (
<ModalPortal>
// show true,falseμ λ°λ₯Έ λͺ¨λ¬μ°½ CSS μ μ©
<div className={`modal ${show ? "show" : ""}`}>
<div className="modal__inner">
<p>μ λ§ κ²°μ ν κΉμ?</p>
<div>
<button onClick={proceed}>μ</button>
<button onClick={cancel}>μλμ€</button>
</div>
</div>
</div>
</ModalPortal>
) : null;
};
β κ²°μ νκΈ°μ λ°λ₯Έ λ°μ΄ν° μ μ§ λ° μμ ꡬν
λ§μ½ κ²°μ νκΈ°λ₯Ό ν΄λ¦νλ©΄ 체ν¬λ μνμ μ₯λ°κ΅¬λμμ μμ νκ³ , μ₯λ°κ΅¬λμλ 체ν¬λμ§μμλ μνλ°μ΄ν°λ€μ΄ λ¨μμμ΄μΌνλ€.
1οΈβ£ Modalμ°½μ μλ₯Ό ν΄λ¦ν κ²½μ° proceedκ° μ€νλλ€.
executePay(ids,... ): checkλμλ μν λ°μ΄ν° idκ°λ€μ μΈμλ‘ μ λ¬.onSucess: ν¨μ μ μ μ€ν ν λμ.setCheckedCartData([]) : 체ν¬μν μ΄κΈ°νalert: κ²°μ μ μμ μΌλ‘ μλ£λμλ€λ μλ¦Όnavigate: μνλͺ©λ‘νμ΄μ§λ‘ μ΄λ, replace trueλ‘ νμ¬ μ΄μ νμ΄μ§λ‘ μ΄λμ μ ν// components/payment/index.tsx
type PaymentInfos = string[];
const Payment = () => {
const navigate = useNavigate();
const [checkedCartData, setCheckedCartData] = useRecoilState(checkedCartState);
...
const { mutate: executePay } = useMutation((ids: PaymentInfos) =>
graphqlFetcher(EXECUTE_PAY, ids)
);
...
const proceed = () => { // 1οΈβ£ λ²
const ids = checkedCartData.map(({ id }) => id);
executePay(ids, {
onSuccess: () => {
setCheckedCartData([]);
alert("κ²°μ μλ£λμμ΅λλ€.");
navigate("/products", { replace: true });
},
});
};
...
};
2οΈβ£ 체ν¬λ μνμ λ°μ΄ν°λ€μ cartData(μ₯λ°κ΅¬λ λ°μ΄ν°)μμ μμ
// src/mocks/handlers.ts
export const handlers = [
...
graphql.mutation(EXECUTE_PAY, ({ variables: ids }, res, ctx) => { // 2οΈβ£ λ²
ids.forEach((id: string) => {
delete cartData[id];
});
return res(ctx.data(ids));
}),
];
// src/graphql/payment.ts import { gql } from "graphql-tag"; export const EXECUTE_PAY = gql` mutation EXECUTE_PAY($ids: [ID!]) { payInfo(info: $info) } `;
β μ ν μν κ²°μ ν μμ λ° λ―Έμ ν μν μ₯λ°κ΅¬λμ μ μ§λλ κ²μ νμΈ
