Will find a way
상태관리 : 약관동의 구현 (모든 약관, 필수약관) / TypeScript, JS 메서드 본문
약관동의 구현 (모든 약관, 필수약관)
React + Typescript 회원가입 기능을 구현하고 있다.
그 중에 하나인 회원가입 전 약관동의를 하는 페이지를 만들었다.
그래서 내가 한 방법을 공유해보려고 한다.
원하던 것
- 모든약관 동의(체크)하면 필수/선택 약관 모두 체크 박스 체크
- 이 중 하나의 약관만 체크가 해제되면 모든 약관 해제
- 모든 약관 + 필수 약관 둘중 하나만 조건 충족되면 다음 페이지 활성화
기능구현
1. 체크박스가 여러개 쓰일 것을 고려하여 컴포넌트로 제작
CheckBox.tsx
type CheckBoxType = {
id: string;
className?: string;
checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const CheckBox = ({ id, className, checked, onChange }: CheckBoxType) => {
// props로 id, className, check 상태, check의 변화를 주는 함수를 받음
return (
<input
id={id}
name={id}
type="checkbox"
checked={checked}
onChange={onChange}
className={`accent-[#2c2c2c] bg-[#dedede] ${className}`}
/>
)
;
};
export default CheckBox;
2. input(checkbox)가 있다면 label도 만들어야함
label 기능을 하는 컴포넌트 제작
TermsLabel.tsx
// 실제 작업할 때는 타입을 관리하는 파일을 따로 별도로 관리했다.
// 코드의 이해를 돕기위해서 타입을 다시 써본다.
type CheckBoxType = {
id:string;
className?: string;
checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
import CheckBox from './CheckBox';
const TermsLabel = ({
id,
checked,
onChange,
label,
}: CheckBox & { label: string } ) => {
return (
<label htmlFor={id}>
<CheckBox
id={id}
checked={checked}
onChange={onChange} />
{label}
</label>
);
export default TermsLabel;
}
새로 알게된 것
const TermsLabel = ({ id, checked, onChange, label, }: CheckBox & { label: string } ...
타입을 2개 이상 적용할 때 & 로 적용
3. 약관 항목이 여러개가 있어서
ul, li와 map을 이용하여 순회하면서 출력할 컴포넌트 제작
TermsUl.tsx
import TermsLabel from './TermsLabel';
const TermsUl = ({
termsText,
checked,
onCheckChange,
isAllChecked
}: {
termsText: object;
checked: Record<string, boolean>;
isAllChecked: boolean;
onCheckChange: {id: string} => void;
}) => {
const tersArr = Object.entries(termsText);
return (
<ul className="flex flex-col gap-y-1">
{termsArr.map(([key, label]) => (
<li className="flex justify-between" key={key}>
<div>
<TermsLabel
id={key}
checked={isAllChecked || checked[key]}
onChange={() => onCheckChange(key)}
label={label}
/>
<div className="text[#777] cursor-pointer>보기</div>
</div>
</li>
))}
</ul>
);
export default TermsUl;
};
Typescript
Record<keys, values>
-> 객체의 key와 value 의 타입을 지정
( Record<string, boolean> : 객체의 key 타입 string, value 타입 boolean)
ex) { 'age' : true }
Javascript
|| 논리 연산자
checked={ isAllChecked || checked[key] }
왼쪽 값이 falsy 면 오른쪽 값을 반환 / 전체체크가 true면 왼쪽 반환을 함
Object.entries()
const termsText = {
age: '[필수] 만 14세 이상입니다.',
terms: '[필수] 이용약관 동의',
};
Object.entries(termsText);
결과
[ ['age', '[필수] 만 14세 이상입니다.'], ['terms', '[필수] 이용약관 동의'] ]
배열의 구조분해 할당
{termsArr.map(([key, label]) => ...
- 'age' -> key
- '[필수] 만 14세 이상입니다.' -> label
4. 전체 페이지 (상단 레이아웃)
TermsOfService.tsx
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import TermsUl from './Termsul';
import CheckBox from './CheckBox';
import Button from '../../components/Button';
const TermsOfService = () => {
const termsText = {
age: '[필수] 만 14세 이상입니다.',
terms: '[필수] 이용약관 동의',
privacy: '[필수] 개인정보 수집 및 이용 동의',
paidContent: '[필수] 유료작품 게시약관 동의',
paidUse: '[필수] 유료이용약관 동의',
thirdParty: '[선택] 개인정보 . 제 3자 제공 동의',
marketing: '[선택] 마케팅 정보 수신 동의',
};
const [checks, setChecks] = useState<Record<string, boolean>>()
Object.keys(termsText).reduce(
(acc, key) => {
return { ...acc, [key]: false };
},
{} as Record<string, boolean>, // type 지정 (안되면 acc의 타입이 추론이 되지 않아 eslint 에러를 띄움)
),
);
// 모든 항목이 체크됐는지 확인 (모두 동의 여부)
const isAllChecked = Object.values(checks).every(Boolean);
// 모든 동의 체크박스의 onChange 핸들러
const handleAllCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
const newChecked = e.target.checked; // 체크 여부 (true/false) 추출
// 모든 약관 체크 상태를 newChecked 값으로 통일해서 덮어쓰기
setChecks((prev) =>
Object.fromEntries( // key-value 쌍을 받아서 객체로 재구성하는 메서드
Object.keys(prev).map((key) => [key, newChecked]);
// key 현재 체크 상태 객체(prev)의 모든 key만 배열로 반환
// map 모든 key에 대해 [key, true/false] 형태의 배열 생성
)
);
};
// 개별 체크박스를 토글하는 핸들러
const handleCheckChange = (id: string) => {
setChecks((prev) => ({
...prev, // 기존 상태 유지
[id]: !prev[id], // 클릭한 체크박스의 상태만 반전 (토글)
}));
};
// '[필수]' 텍스트가 포함된 약관 key만 필터링 (필수 항목 리스트 생성)
const requiredKeys = Object.entries(termsText).filter(([_, value]) => value.includes('[필수]')).map(([key]) => key);
const isRequiredAllChecked = requiredKeys.every((key) => checks[key]);
return (
<div className="w-[100%] h-screen relative">
<div className="w-[300px] border-[2px] rounded-[4px] p-5 absolute left-[50%] top-[50%] translate-[-50%] flex flex-col gap-2.5 box-content">
<h3 className="text-[1.5rem] font-bold">
이용약관에
<br />
동의해주세요
</h3>
<div>
<CheckBox
id="allChecked"
checked={isAllChecked}
onChange={handleAllCheck}
/>{' '}
아래 약관에 모두 동의합니다.
</div>
<form className="flex flex-col gap-y-2.5 border-t-2 border-[#999] py-3">
<TermsUl
termsText={termsText}
checked={checks}
onCheckChange={handleCheckChange}
isAllChecked={isAllChecked}
/>
<Button className={`text-[1.25rem] rounded-[4px] ${isRequiredAllChecked ? 'bg-[#2c2c2c] text-[#dedede]' : 'text-[#2c2c2c] bg-[#dedede]'}`}>
{isRequiredAllChecked ? (
<Link to="/auth/signup/account" className="block w-full py-2.5">
다음
</Link>
) : (
<span className="block py-2.5">다음</span>
)}
</Button>
</form>
<p className="text-[#888] text-[14px]">
선택 항목에 동의하지 않아도 서비스 이용이 가능하며, 개인정보 수집 및
이용에 대한 동의를 거부할 권리가 있습니다. 동의 거부 시 회원제 서비스
이용이 제한됩니다.
</p>
</div>
</div>
);
};
초기 상태 생성 (useState)
Object.keys(termsText).reduce((acc, key) => ({ ...acc, [key]: false }), {});
- termsText의 key들 (age, terms, ...)을 전부 false로 초기화
- checks = { age: false, terms: false, ... }
전체 체크 여부 확인
const isAllChecked = Object.values(checks).every(Boolean);
- checks의 모든 value가 true인지 검사
- 전체 동의 체크박스 상태 결정에 사용
모두 동의 체크 시 상태 일괄 업데이트
Object.keys(prev).map((key) => [key, newChecked])
- 모든 key에 대해 [key, true/false] 배열 생성
- Object.fromEntries()로 다시 객체화 -> { age: true, terms: true, ... }
개별 체크 시 해당 key의 값만 반전
[id]: !prev[id]
- 기존 상태를 유지하고, 해당 체크 항목만 true/false 토글
필수 항목만 추출
Object.entries(termsText).filter(([_, value]) => value.includes('필수')).map(([key]) => key);
- termsText 중 [필수]가 들어간 항목만 골라 key 배열로 저장
필수 항목만 전부 체크됐는지 확인
requiredKeys.every((key) => checks[key]);
삼항연산자 ( 필수 항목 체크시 다음 버튼 활성화)
<button>
{isRequiredAllChecked ? ( <Link to="/" className="block w-full py-2.5">
다음
</Link> ) : (
<span className="block py-2.5">
다음
</span> )}
</button>
마무리
늘 생각하는거지만 코드를 짜면서 내가 기능을 구현하기 위해 어떤것이 필요한지 알아내는 것이 중요하다는 것을 이번 기능을 구현하면서 중요하다는 것을 느꼈고 역시 기본기의 중요성을 다시 한번 느끼게 됐다. 지금 정리하면서도 완벽히 이해 안 된것이 있다. 그것 또한 기본기가 부족해서 생겨난 일이라고 생각한다. 블로그에 올렸지만 다시 한번 정리해보려고 한다.
'FrontEnd > React' 카테고리의 다른 글
React-Hook-Form(RHF) : Form을 편하게 관리해보자 (Feat. Zod) (0) | 2025.05.11 |
---|---|
새로운 라우터가 등장했다고? (createBrowserRouter) (0) | 2025.04.10 |
[React] Zustand 에 대해서 (React 상태관리) (0) | 2025.04.01 |
[React] Vite에 대해서 (1) | 2025.01.29 |
[React] Redux / Reducer를 이용한 예제 (1) | 2024.10.22 |