5. STATE MANAGEMENT

Hapjeong Girl·2022년 10월 4일
0

MUTSA_STUDY

목록 보기
5/11
post-thumbnail

5.0 Dark Mode part One

Recoil?

React JS에서 사용할 수 있는 상태 관리 라이브러리

라이트 모드를 추가해보자

  1. Index.tsx에 있는 ThemeProvider를 App component로 옮기자

    // App.tsx
    function App() {
    	return (
    		<>
    			<ThemeProvider theme={theme}>
    				<GlobalStyle />
    				<Router />
    				<ReactQueryDevtools initialIsOpen={true} />
    			</ThemeProvider>
    		</>
    	);
    }
    export default App;
  2. 테마를 추가해보자

    // Theme.ts
    import { DefaultTheme } from 'styled-components';
    
    export const darkTheme: DefaultTheme = {
    	bgColor: '#2f3640',
    	textColor: '#f5f6fa',
    	accentColor: '#9c88ff'
    };
    
    export const lightTheme: DefaultTheme = {
    	bgColor: 'whitesmoke',
    	textColor: 'black',
    	accentColor: '#9c88ff'
    };

테마를 전환할 수 있는 스위치를 만들어보자

  1. 현재 모드 state를 만들자

    const [isDark, setIsDark] = useState(false);
  2. isDark 토글 함수를 만들자

    const toggleDark = () => setIsDark((current) => !current);
  3. 버튼을 만들고, 클릭 시 함수를 집어넣자

    return (
    		<>
    			<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
    				<button onClick={toggleDark}>Toggle Mode</button>
    				<GlobalStyle />
    				<Router />
    				<ReactQueryDevtools initialIsOpen={true} />
    			</ThemeProvider>
    		</>
    	);

5.1 Dark Mode part Two

버튼을 Coins.tsx로 옮기고 함수를 전달해주자

  1. Router에 인터페이스를 추가하고 Coins에 전달하자

    interface IRouterProps {
    	toggleDark: () => void;
    }
    
    function Router({ toggleDark }: IRouterProps) {
    	return (
    		<BrowserRouter>
    			<Switch>
    				<Route path='/:coinId'>
    					<Coin />
    				</Route>
    				<Route path='/'>
    					<Coins toggleDark={toggleDark} />
    				</Route>
    			</Switch>
    		</BrowserRouter>
    	);
    }
  2. Coins에서 toggleDark 함수를 전달받자

    interface ICoinsProps {
    	toggleDark: () => void;
    }
    
    function Coins({ toggleDark }: ICoinsProps) {
    	const { isLoading, data } = useQuery<ICoin[]>('allCoins', fetchCoins);
    		return (
    			<Container>
    				<Helmet>
    					<title>코인</title>
    				</Helmet>
    				<Header>
    					<Title>코인</Title>
    					<button onClick={toggleDark}>Toggle Dark Mode</button>

Chart에게 다크모드인지를 보내주자

  1. App → Router로 isDark를 보내주자

    function App() {
    	const [isDark, setIsDark] = useState(false);
    	const toggleDark = () => setIsDark((current) => !current);
    	return (
    		<>
    			<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
    				<GlobalStyle />
    				<Router isDark={isDark} toggleDark={toggleDark} />
  2. Router → Coin으로 isDark를 보내주자

    interface IRouterProps {
    	toggleDark: () => void;
    	isDark: boolean;
    }
    
    function Router({ toggleDark, isDark }: IRouterProps) {
    	return (
    		<BrowserRouter>
    			<Switch>
    				<Route path='/:coinId'>
    					<Coin isDark={isDark} />
    				</Route>
    				<Route path='/'>
    					<Coins toggleDark={toggleDark} />
    				</Route>
    			</Switch>
    		</BrowserRouter>
    	);
    }
    export default Router;
  3. Coin → Chart로 isDark를 보내주자

    interface ICoinProps {
    	isDark: boolean;
    }
    
    function Coin({ isDark }: ICoinProps) {
    	...
    	..
    	.
    	<Switch>
    		<Route path={`/:coinId/price`}>
    			<Price />
    		</Route>
    		<Route path={`/:coinId/chart`}>
    			<Chart isDark={isDark} coinId={coinId} />
    		</Route>
    	</Switch>
  4. Chart에서 isDark를 받아 차트에 적용하자

    interface CharProps {
    	coinId: string;
    	isDark: boolean;
    }
    
    function Chart({ coinId, isDark }: CharProps) {
    	...
    	..
    	.
    	<ApexChart
    					type='line'
    					series={[{ name: 'Price', data: data?.map((price) => price.close) ?? [] }]}
    					options={{
    						theme: { mode: isDark ? 'dark' : 'light' },

문제

옮기고 옮기고 또 옮기고… 반복 작업 발생, 복잡.. ⇒ 상태관리를 사용하자

5.2 Introduction to Recoil

component를 atom의 value로 연결하자

상태관리 미사용

상태관리 사용

기존에서는 isDark가 필요없는 Router, Coin도 전달을 하기 위해 isDark를 가져가야 했다.

허나, 상태관리는 독립적인 공간에 컴포넌트(atom)를 저장해놔 필요한 애들만 쓸 수 있게 해준다!

Recoil을 시작하자

  1. Recoil 라이브러리 설치 : npm install recoil

  2. Index.tsx에서 RecoilRoot로 감싸주자

    ReactDOM.render(
    	<React.StrictMode>
    		<RecoilRoot>
    			<QueryClientProvider client={queryClient}>
    				<App />
    			</QueryClientProvider>
    		</RecoilRoot>
    	</React.StrictMode>,
    	document.getElementById('root')
    );

atom으로부터 오는 value로 바꾸자

  1. atom 정의 : atom({ key : 유일한 이름, default : 기본값})

    // atom.ts -> src 폴더 내
    import { atom } from 'recoil';
    
    export const isDarkAtom = atom({
    	key: 'isDark',
    	default: false
    });
  2. atom을 App과 연결하자

    useRecoilValue hook 사용 ⇒ useRecoilValue(가져오고자 하는 atom명)

    import { useRecoilValue } from 'recoil';
    import { isDarkAtom } from './atoms';
    
    function App() {
    	const isDark = useRecoilValue(isDarkAtom);
    	return (
    		<>
    			<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
    				<GlobalStyle />
    				<Router />
    				<ReactQueryDevtools initialIsOpen={true} />
    			</ThemeProvider>
    		</>
    	);
    }
  3. 마찬가지로 atom을 Chart에서 사용하자

5.3 Introduction to Recoil part Two

atom의 value를 수정하자 ⇒ Coins에서 모드 변경 함수를 만들자

atom 변경 함수를 만들자

useSetRecoilState hook 사용 ⇒ useSetRecoilState(수정하고자 하는 atom명)

// Coins.tsx
const setDarkAtom = useSetRecoilState(isDarkAtom);
const toggleDarkAtom = () => setDarkAtom((prev) => !prev);

<Header>
	<Title>코인</Title>
	<button onClick={toggleDarkAtom}>Toggle Mode</button>
</Header>

5.5 To do Setup

To do 리스트를 빌드해보자

지금까지 했던 비트코인 페이지는 다 지웁니다!

  1. ToDoList.tsx 생성하자

    function ToDoList() {
    	return null;
    }
    
    export default ToDoList;
  2. App에 ToDoList를 렌더링하자

    function App() {
    	return (
    		<>
    			<GlobalStyle />
    			<ToDoList />
    		</>
    	);
    }
    export default App;
  3. Form을 작성하자

    // ToDoList.tsx
    import { useState } from 'react';
    
    function ToDoList() {
    	const [toDo, setToDo] = useState('');
    	const onChange = (event: React.FormEvent<HTMLInputElement>) => {
    		const {
    			currentTarget: { value }
    		} = event;
    		setToDo(value);
    	};
    
    	const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    		event.preventDefault();
    		console.log(toDo);
    	};
    	return (
    		<div>
    			<form onSubmit={onSubmit}>
    				<input onChange={onChange} value={toDo} placeholder='Write to do' />
    				<button>Add</button>
    			</form>
    		</div>
    	);
    }
    
    export default ToDoList;

    ⇒ input이 여러개면 같은 함수, state를 여러 개 만들어야함..

5.6 Forms

react-hook-form에 대해 배우자

React-hook-form을 사용하자

  1. 설치하자 : npm install react-hook-form

  2. useForm을 import 하자

    import { useForm } from 'react-hook-form';
  3. **useForm hook을 사용하자**

    register 함수 : onChange 이벤트 핸들러, props, setstate를 대체

    function ToDoList() {
    	const { register } = useForm();
    	console.log(register('To do'));
    	return (
    		<div>
    			<form>
    				<input placeholder='Write to do' />
    				<button>Add</button>
    			</form>
    		</div>
    	);
    }
    
    export default ToDoList;

    +) onBlur ? → input 창 밖을 클릭 시

  4. register가 반환하는 객체를 input의 props로 넘겨주자

    function ToDoList() {
    	const { register } = useForm();
    	return (
    		<div>
    			<form>
    				<input {...register('toDo')} placeholder='Write to do' />
    				<button>Add</button>
    			</form>
    		</div>
    	);
    }

    ⇒ 아직은 잘 작동하는지 확인 불가능하다

  5. useFormwatch를 사용하자

    watch 함수 : 입력한 form 입력값들의 변화를 관찰할 수 있게 해주는 함수

    function ToDoList() {
    	const { register, watch } = useForm();
    	console.log(watch());
    	return (
    		<div>
    			<form>
    				<input {...register('toDo')} placeholder='Write to do' />
    				<button>Add</button>
    			</form>
    		</div>
    	);
    }

5.7 Form Validation

form의 onSubmit을 react-form-hook을 사용해 대체해보자

handleSubmit 사용

handleSubmit : validation, preventDefault 등 담당

handleSubmit(데이터가 유효할 때 실행되는 함수, 데이터가 유효하지 않을 때 실행되는 함수)

function ToDoList() {
	const { register, watch, handleSubmit } = useForm();
	const onValid = (data: any) => {
		console.log(data);
	};
	return (
		<div>
			<form onSubmit={handleSubmit(onValid)}>
				<input {...register('toDo', { required: true })} placeholder='Write to do' />
				<button>Add</button>
			</form>
		</div>
	);
}

validation을 추가해보자

<input {...register('toDo', { required: true, minLength: 10 })} placeholder='Write to do' />

에러를 발생시켜보자

const { register, watch, handleSubmit, formState } = useForm();

console.log(formState.errors);

단순히 에러가 있다는 사실뿐만이 아니라, 어떤 종류의 에러가 생겼는지도 알 수 있다.

에러 메세지를 출력해보자

function ToDoList() {
	const { register, watch, handleSubmit, formState } = useForm();
	const onValid = (data: any) => {
		console.log(data);
	};
	console.log(formState.errors);
	return (
		<div>
			<form style={{ display: 'flex', flexDirection: 'column' }} onSubmit={handleSubmit(onValid)}>
				<input
					{...register('toDo', {
						required: 'To-Do is required',
						minLength: {
							value: 5,
							message: 'Too Short'
						}
					})}
					placeholder='Write to do'
				/>
				<button>Add</button>
			</form>
		</div>
	);
}

5.8 Form Errors

정규식(RegExp)을 사용해 validation을 구현해보자

이메일을 정규식으로 검사해보자

  1. pattern을 넘기자
    1. 바로 값을 보내자

      <input
      	{...register('email', {
      		required: 'Email is required',
      		pattern: /^[A-Za-z0-9._%+-]+@naver.com/
      	})}
      	placeholder='Write to do'
      />
    2. 객체에다 넣어서 보내자

      <input
      	{...register('email', {
      		required: 'Email is required',
      		pattern: { value: /^[A-Za-z0-9._%+-]+@naver.com/, message: 'Only naver.com emails allowed' }
      	})}
      	placeholder='Write Email'
      />

사용자에게 에러메시지를 띄워주자

<span>{errors?.email?.message}</span>

TypeScript에게 작성한 Form 모양을 알려주자

data → any로 해놓는 건 타입스크립트의 보호기능을 사용하기에 좋지 않다

form의 인터페이스를 만들자

interface IForm {
	toDo: string;
	email: string;
}

const {
		register,
		watch,
		handleSubmit,
		formState: { errors }
	} = useForm<IForm>();

// 필수가 아니라면 ?를 붙이자 -> ex) nickname?: string;

5.9 Custom Validation

어떻게 에러를 발생시킬 수 있는지 알아보고, validation을 만들어보자

password와 password1이 다를 시, 에러를 발생시켜보자

  1. validation의 데이터 타입을 any에서 IForm으로 변경하자

    const onValid = (data: IForm) => {
    	console.log(data);
    };
  2. setError를 가져오자

    setError : 특정한 에러를 발생시키게 해준다.

    const {
    	register,
    	watch,
    	handleSubmit,
    	formState: { errors },
    	setError
    } = useForm<IForm>({ defaultValues: { email: '@naver.com' } });
  3. password와 password1을 비교해 setError를 발생시키자

    const onValid = (data: IForm) => {
    	if (data.password !== data.password1) {
    		setError('password1', { message: 'Password are not the same' });
    	}
    };

Form 전체에 대해 에러를 발생시켜보자

  1. form 인터페이스에 항목을 추가하자

    interface IForm {
    	toDo: string;
    	email: string;
    	password: string;
    	password1: string;
    	extraError?: string;
    }
  2. 에러를 추가하자

    const onValid = (data: IForm) => {
    	if (data.password !== data.password1) {
    		setError('password1', { message: 'Password are not the same' });
    	}
    	setError('extraError', { message: 'Server Offline.' });
    };
  3. 에러를 보여주자

    <span>{errors?.extraError?.message}</span>

+) setError는 form에서 특정 input에 강제로 포커싱할 수 있다.

const onValid = (data: IForm) => {
	if (data.password !== data.password1) {
		setError('password1', { message: 'Password are not the same' }, { shouldFocus: true });
	}
	setError('extraError', { message: 'Server Offline.' });
};

특정 규칙을 만들어보자

→ toDo에 alcohol이 포함되어 있으면 금지시키자

  1. validate 옵션을 사용하자

    ⇒ validate : (현재 쓰여지고 있는 값)

    <input
    	{...register('toDo', {
    		required: 'To-Do is required',
    		minLength: {
    			value: 5,
    			message: 'Too Short'
    		},
    		validate: (value) => !value.includes('alcohol')
    	})}
    	placeholder='Write to do'
    />
  2. 사용자에게 에러메시지를 띄우자

    <input
    	{...register('toDo', {
    		required: 'To-Do is required',
    		minLength: {
    			value: 5,
    			message: 'Too Short'
    		},
    		validate: (value) => (value.includes('alcohol') ? 'No Alcohol allowed' : true)
    	})}
    	placeholder='Write to do'
    />
    <span>{errors?.toDo?.message}</span>

  3. validate 객체로 변경해보자

    ⇒ 규칙 여러개를 생성할 수 있다

    <input
    	{...register('toDo', {
    		required: 'To-Do is required',
    		minLength: {
    			value: 5,
    			message: 'Too Short'
    		},
    		validate: { noAlcol: (value) => (value.includes('alcohol') ? 'No Alcohol allowed' : true) }
    	})}
    	placeholder='Write to do'
    />

+) Submit 후 값을 초기화하자

const { register, handleSubmit, setValue } = useForm<IForm>();
const handleValid = (data: IForm) => {
	console.log('add to do', data.toDo);
	setValue('toDo', '');
};

5.11 Add To Do

  1. toDoState를 만들자

    const toDoState = atom({
    	key: 'toDo',
    	default: []
    });
  1. toDoState 값을 가져오자 ⇒ useRecoilValue

    const value = useRecoilValue(toDoState);
  1. toDoState 값을 수정하자 ⇒ useSetRecoilState

    const modFn = useSetRecoilState(toDoState);

+) 한번에 값과 수정하기를 해보자

useRecoilState 사용 : value와 modifier 함수를 반환한다.

const [toDos, setToDos] = useRecoilState(toDoState);
  1. TypeScript에게 배열의 타입을 알려주자

    interface IToDo {
    	text: string;
    	category: 'TO_DO' | 'DOING' | 'DONE';
    }
    
    const toDoState = atom<IToDo[]>({
    	key: 'toDo',
    	default: []
    });

    ⇒ 카테고리는 세가지 문자열만 갖게 된다!

폼이 제출되고 데이터가 유효하면 state(상태)를 바꾸자

const handleValid = ({ toDo }: IForm) => {
	console.log('add to do', toDo);
	setToDos((oldToDos) => [{ text: toDo, category: 'TO_DO' }, ...oldToDos]);
	setValue('toDo', '');
};

hello 라고 입력하면?

ToDo를 ul에다가 그려주자

<ul>
	{toDos.map((toDo) => (
		<li key={toDo.id}>{toDo.text}</li>
	))}
</ul>

5.12 Refactoring

코드를 리펙토링 해보자

  1. CreateToDo.tsx 생성

    import { useForm } from 'react-hook-form';
    import { useSetRecoilState } from 'recoil';
    import { toDoState } from '../atom';
    
    interface IForm {
    	toDo: string;
    }
    
    function CreateToDo() {
    	const setToDos = useSetRecoilState(toDoState);
    	const { register, handleSubmit, setValue } = useForm<IForm>();
    	const handleValid = ({ toDo }: IForm) => {
    		setToDos((oldToDos) => [{ text: toDo, id: Date.now(), category: 'TO_DO' }, ...oldToDos]);
    		setValue('toDo', '');
    	};
    	return (
    		<form onSubmit={handleSubmit(handleValid)}>
    			<input {...register('toDo', { required: 'Please write a To Do' })} placeholder='Write to do' />
    			<button>Add</button>
    		</form>
    	);
    }
    
    export default CreateToDo;
  2. ToDo.tsx 생성

    import { IToDo } from '../atom';
    
    function ToDo({ text }: IToDo) {
    	return <li>{text}</li>;
    }
    
    export default ToDo;
  1. ToDoList.tsx 수정

    import { useRecoilValue } from 'recoil';
    import { toDoState } from '../atom';
    import CreateToDo from './CreateToDo';
    import ToDo from './ToDo';
    
    function ToDoList() {
    	const toDos = useRecoilValue(toDoState);
    
    	return (
    		<div>
    			<h1>To Dos</h1>
    			<hr />
    			<CreateToDo />
    			<ul>
    				{toDos.map((toDo) => (
    					<ToDo key={toDo.id} {...toDo} />
    				))}
    			</ul>
    		</div>
    	);
    }
    
    export default ToDoList;
  2. 카테고리를 위해 ToDo.tsx에 버튼을 추가하자

    import { IToDo } from '../atom';
    
    function ToDo({ text }: IToDo) {
    	return (
    		<li>
    			<span>{text}</span>
    			<button>ToDo</button>
    			<button>Doing</button>
    			<button>Done</button>
    		</li>
    	);
    }
    
    export default ToDo;

5.13 Categories

사용자들이 버튼을 이용해서 toDo의 카테고리를 바꿀 수 있게 하자

  1. 카테고리를 체크하자

    import { IToDo } from '../atom';
    
    function ToDo({ text, category }: IToDo) {
    	return (
    		<li>
    			<span>{text}</span>
    			{category !== 'TO_DO' && <button>ToDo</button>}
    			{category !== 'DOING' && <button>Doing</button>}
    			{category !== 'DONE' && <button>Done</button>}
    		</li>
    	);
    }
    
    export default ToDo;
  1. 카테고리를 바꾸는 함수를 만들자

    1. 방법 1

      💡 인자가 있는 함수는 **() ⇒ 함수(인자)** 이런 형태로 쓴다!
      import { IToDo } from '../atom';
      
      function ToDo({ text, category }: IToDo) {
      	const onClick = (newCategory: IToDo['category']) => {
      		console.log('i wanna to', newCategory);
      	};
      	return (
      		<li>
      			<span>{text}</span>
      			{category !== 'TO_DO' && <button onClick={() => onClick('TO_DO')}>ToDo</button>}
      			{category !== 'DOING' && <button onClick={() => onClick('DOING')}>Doing</button>}
      			{category !== 'DONE' && <button onClick={() => onClick('DONE')}>Done</button>}
      		</li>
      	);
      }
      
      export default ToDo;
    2. 방법 2

      function ToDo({ text, category }: IToDo) {
      	const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      		console.log('i wanna go', event.currentTarget.name);
      	};
      	return (
      		<li>
      			<span>{text}</span>
      			{category !== 'TO_DO' && (
      				<button name='TO_DO' onClick={onClick}>
      					ToDo
      				</button>
      			)}
      			{category !== 'DOING' && (
      				<button name='DOING' onClick={onClick}>
      					Doing
      				</button>
      			)}
      			{category !== 'DONE' && (
      				<button name='DONE' onClick={onClick}>
      					Done
      				</button>
      			)}
      		</li>
      	);
      }
  2. atom을 수정하게 만들자

    const setToDos = useSetRecoilState(toDoState);

5.14 Immutability part One

  1. 수정하고자 하는 to do의 경로를 찾자

    const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    	const {
    		currentTarget: { name }
    	} = event;
    	setToDos((oldToDos) => {
    		const targetIndex = oldToDos.findIndex((toDo) => toDo.id === id);
    		console.log(targetIndex);
    		return oldToDos;
    	});
    };
  2. 새로운 to do를 만들어서 원래 to do를 업데이트하자

    const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    	const {
    		currentTarget: { name }
    	} = event;
    	setToDos((oldToDos) => {
    		const targetIndex = oldToDos.findIndex((toDo) => toDo.id === id);
    		const oldToDo = oldToDos[targetIndex];
    		const newToDo = { text, id, category: name };
    		console.log('replace the to do in the index', targetIndex, 'with', newToDo);
    		return oldToDos;
    	});
    };

5.15 Immutability part Two

배열의 원소를 어떻게 교체하는지에 대해 배우자

배열의 원소를 교체하는 방법

slice를 사용하자

const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
	const {
		currentTarget: { name }
	} = event;
	setToDos((oldToDos) => {
		const targetIndex = oldToDos.findIndex((toDo) => toDo.id === id);
		const oldToDo = oldToDos[targetIndex];
		const newToDo = { text, id, category: name as any };
		console.log('replace the to do in the index', targetIndex, 'with', newToDo);
		return [...oldToDos.slice(0, targetIndex), newToDo, ...oldToDos.slice(targetIndex + 1)];
	});
};
💡 **as any** ? 타입스크립트에게 확인할 필요 없다고 알려줌

5.16 Selectors part One

Recoil의 selector 개념에 대해 공부하자

Selector : atom의 output을 변형시키는 도구

Selector을 만들어보자

  1. atom.tsx

    import { atom, selector } from 'recoil';
    
    export const toDoSelector = selector({
    	key: 'toDoSelector',
    	get: ({ get }) => {
    		const toDos = get(toDoState);
    		return toDos.length;
    	}
    });
  2. selectorOutput을 얻어보자

    const selectorOutput = useRecoilValue(toDoSelector);

  3. 필터링해 조건을 만족하는 to do만 얻어내자

    export const toDoSelector = selector({
    	key: 'toDoSelector',
    	get: ({ get }) => {
    		const toDos = get(toDoState);
    		return [toDos.filter((toDo) => toDo.category === 'TO_DO'), toDos.filter((toDo) => toDo.category === 'DOING'), toDos.filter((toDo) => toDo.category === 'DONE')];
    	}
    });

세 각기 다른 배열을 렌더하자

  1. 배열을 쪼개자

    const [toDo, doing, done] = useRecoilValue(toDoSelector);
  1. 나눠서 보여주자

    function ToDoList() {
    	const [toDo, doing, done] = useRecoilValue(toDoSelector);
    	return (
    		<div>
    			<h1>To Dos</h1>
    			<hr />
    			<CreateToDo />
    			<h2>To Do</h2>
    			<ul>
    				{toDo.map((toDo) => (
    					<ToDo key={toDo.id} {...toDo} />
    				))}
    			</ul>
    			<hr />
    			<h2>Doing</h2>
    			<ul>
    				{doing.map((toDo) => (
    					<ToDo key={toDo.id} {...toDo} />
    				))}
    			</ul>
    			<hr />
    			<h2>Done</h2>
    			<ul>
    				{done.map((toDo) => (
    					<ToDo key={toDo.id} {...toDo} />
    				))}
    			</ul>
    		</div>
    	);
    }

5.17 Selectors part Two

한번에 하나의 카테고리만 보여주자

사용자가 선택한 카테고리를 저장하는 state를 만들자

  1. state 추가하자

    // atom.ts
    export const categoryState = atom({
    	key: 'category',
    	default: 'TO_DO'
    });
  2. select 추가하자

    function ToDoList() {
    	const [toDo, doing, done] = useRecoilValue(toDoSelector);
    	return (
    		<div>
    			<h1>To Dos</h1>
    			<hr />
    			<select>
    				<option value='TO_DO'>To Do</option>
    				<option value='DOING'>Doing</option>
    				<option value='DONE'>Done</option>
    			</select>
    			<CreateToDo />
    		</div>
    	);
    }
  1. select의 이벤트를 감지하자

    const onInput = (event: React.FormEvent<HTMLSelectElement>) => {
    	console.log(event.currentTarget.value);
    };
  2. value를 categoryState atom과 연결하자

    function ToDoList() {
    	const [toDo, doing, done] = useRecoilValue(toDoSelector);
    	const [category, setCategory] = useRecoilState(categoryState);
    	const onInput = (event: React.FormEvent<HTMLSelectElement>) => {
    		setCategory(event.currentTarget.value);
    	};
    
    	return (
    		<div>
    			<h1>To Dos</h1>
    			<hr />
    			<select value={category} onInput={onInput}>
    				<option value='TO_DO'>To Do</option>
    				<option value='DOING'>Doing</option>
    				<option value='DONE'>Done</option>
    			</select>
    			<CreateToDo />
    		</div>
    	);
    }
  3. 카테고리를 분류해서 보여주자

    {category === 'TO_DO' && toDo.map((aToDo) => <ToDo key={aToDo.id} {...aToDo} />)}
    {category === 'DOING' && doing.map((aToDo) => <ToDo key={aToDo.id} {...aToDo} />)}
    {category === 'DONE' && done.map((aToDo) => <ToDo key={aToDo.id} {...aToDo} />)}

Selector 내부의 여러 atom들을 가져오자

// atom.tsx
export const toDoSelector = selector({
	key: 'toDoSelector',
	get: ({ get }) => {
		const toDos = get(toDoState);
		const category = get(categoryState);
		return toDos.filter((toDo) => toDo.category === category);
	}
});
function ToDoList() {
	const toDos = useRecoilValue(toDoSelector);
	const [category, setCategory] = useRecoilState(categoryState);
	const onInput = (event: React.FormEvent<HTMLSelectElement>) => {
		setCategory(event.currentTarget.value);
	};

	return (
		<div>
			<h1>To Dos</h1>
			<hr />
			<select value={category} onInput={onInput}>
				<option value='TO_DO'>To Do</option>
				<option value='DOING'>Doing</option>
				<option value='DONE'>Done</option>
			</select>
			<CreateToDo />
			{toDos?.map((toDo) => (
				<ToDo key={toDo.id} {...toDo} />
			))}
		</div>
	);
}

5.18 Enums

toDo의 카테고리가 categoryState에 따라서 추가되게 해보자

  1. CreateToDo.tsx 수정하자

    const category = useRecoilValue(categoryState);
    
    const handleValid = ({ toDo }: IForm) => {
    	setToDos((oldToDos) => [{ text: toDo, id: Date.now(), category: category }, ...oldToDos]);
    	setValue('toDo', '');
    };
    ⛔ **에러발생 :** category는 string인데 toDo의 카테고리는 세 종류로 제한됐기 때문
// atom.tsx
type categories = 'TO_DO' | 'DOING' | 'DONE';

export interface IToDo {
	text: string;
	id: number;
	category: categories;
}
// ToDoList.tsx
function ToDoList() {
	const toDos = useRecoilValue(toDoSelector);
	const [category, setCategory] = useRecoilState(categoryState);
	const onInput = (event: React.FormEvent<HTMLSelectElement>) => {
		setCategory(event.currentTarget.value as any);
	};
// ToDo.tsx
function ToDo({ text, category, id }: IToDo) {
	const setToDos = useSetRecoilState(toDoState);
	const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
		const {
			currentTarget: { name }
		} = event;
		setToDos((oldToDos) => {
			const targetIndex = oldToDos.findIndex((toDo) => toDo.id === id);
			const oldToDo = oldToDos[targetIndex];
			const newToDo = { text, id, category: name as any };
			console.log('replace the to do in the index', targetIndex, 'with', newToDo);
			return [...oldToDos.slice(0, targetIndex), newToDo, ...oldToDos.slice(targetIndex + 1)];
		});

enum을 만들어보자

  1. enum 만들자

    // atom.tsx
    export enum Categories {
    	'TO_DO',
    	'DOING',
    	'DONE'
    }
    
    export interface IToDo {
    	text: string;
    	id: number;
    	category: Categories;
    }
    
    export const categoryState = atom<Categories>({
    	key: 'category',
    	default: Categories.TO_DO
    });
  2. enum을 atom 밖에서 사용해보자

    // ToDoList.tsx
    <select value={category} onInput={onInput}>
    	<option value={Categories.TO_DO}>To Do</option>
    	<option value={Categories.DOING}>Doing</option>
    	<option value={Categories.DONE}>Done</option>
    </select>
    return (
    		<li>
    			<span>{text}</span>
    			{category !== Categories.TO_DO && (
    				<button name={Categories.TO_DO} onClick={onClick}>
    					ToDo
    				</button>
    			)}
    			{category !== Categories.DOING && (
    				<button name={Categories.DOING} onClick={onClick}>
    					Doing
    				</button>
    			)}
    			{category !== Categories.DONE && (
    				<button name={Categories.DONE} onClick={onClick}>
    					Done
    				</button>
    			)}
    		</li>
    	);
    }
    ⛔ **오류발생** : 사실 enum의 값들은 숫자로 저장되어있다.

    ⇒ Categories.TO_DO 값을 문자열로 바꿔주자

    // atom.tsx
    export enum Categories {
    	'TO_DO' = 'TO_DO',
    	'DOING' = 'DOING',
    	'DONE' = 'DONE'
    }

    5.19 Recap

    💪 Challenge

    • toDo 삭제 버튼을 만들어보자
    • localStorage를 이용해 저장 기능을 구현해보자
profile
프론트엔드 / 컴퓨터공학과 4학년

0개의 댓글