Task 4: Timer
Atom 정의
//atoms.ts
import { atom } from "jotai";
const baseDurationAtom = atom(15);
const elapsedTimeAtom = atom(0);
const timerAtom = atom<{ id: number; started: number } | null>(null);
// 이전 예제와의 다른 점은 action을 파마리터로 받습니다.
// action에 따라 분기처리를 한 모습입니다.
const startTimerAtom = atom(null, (get, set, action: "start" | "stop") => {
if (action === "start") {
if (get(timerAtom) !== null) {
// 타이머가 이미 시작 되었을 경우 return
return;
}
if (get(elapsedTimeAtom) >= get(baseDurationAtom)) {
// 타이머가 끝나있을 경우 return
return;
}
const tick = () => {
const now = performance.now() / 1000;
const timer = get(timerAtom);
if (timer) {
set(elapsedTimeAtom, now - timer.started);
}
const elapsedTime = get(elapsedTimeAtom);
if (elapsedTime >= get(baseDurationAtom)) {
set(timerAtom, null); // 타이머를 멈출 때엔 timerAtom에 null을 넣어줍니다.
} else {
// 0.1초마다 timerAtom이 변경됩니다.
set(timerAtom, {
started: timer ? timer.started : now - elapsedTime,
id: setTimeout(tick, 100)
});
}
};
tick(); // 타이머 시작
}
if (action === "stop") {
const timer = get(timerAtom);
if (timer) {
//timerAtom null로 초기화
clearTimeout(timer.id);
set(timerAtom, null);
}
}
});
// onMount: 해당 Atom이 사용되기 시작할 때, 수행되는 작업 설정
// 여기서는 "start" action 실행, unMount될 때엔 "stop" action 실행
startTimerAtom.onMount = (dispatch) => {
dispatch("start");
return () => dispatch("stop");
};
// 수정된 duration의 길이를 통해 elpasedTime을 수정합니다.
// proportion은 timer의 스타일링을 위해 반환합니다.
export const elapsedAtom = atom((get) => {
get(startTimerAtom); // add dependency
const duration = get(baseDurationAtom);
const elapsedTime = get(elapsedTimeAtom);
let proportion = elapsedTime / duration;
if (proportion > 1) {
proportion = 1;
}
return {
elapsedTime: Math.min(elapsedTime, duration),
proportion
};
});
// duration을 반환하고,
// 프로그레스바로 수정된 duration 값을 baseDurationAtom에 저장하고, timerAtom을 실행시킵니다.
export const durationAtom = atom(
(get) => get(baseDurationAtom),
(_get, set, duration: number) => {
set(baseDurationAtom, duration);
set(startTimerAtom, "start");
}
);
// elapsedTime을 초기화하고, timerAtom을 재실행시킵니다.
export const resetAtom = atom(null, (get, set) => {
set(startTimerAtom, "stop");
set(elapsedTimeAtom, 0);
set(startTimerAtom, "start");
});
Atom 적용
import { useAtom } from "jotai";
import { elapsedAtom, durationAtom, resetAtom } from "./atoms";
const ElapsedTime = () => {
const [{ elapsedTime, proportion }] = useAtom(elapsedAtom);
return (
<div>
<span>Elapsed Time:</span>
<div
style={{
width: "100%",
border: "1px solid gray"
}}
>
<div
style={{
width: `${proportion * 100}%`,
height: "100%",
backgroundColor: "lightblue"
}}
/>
</div>
<span>{elapsedTime.toFixed(1)}s</span>
</div>
);
};
const Duration = () => {
const [duration, setDuration] = useAtom(durationAtom);
return (
<div>
<span>Duration:</span>
<input
type="range"
value={duration}
onChange={(e) => {
setDuration(Number(e.target.value));
}}
min={0}
max={30}
step={0.1}
/>
</div>
);
};
const Reset = () => {
const [, reset] = useAtom(resetAtom);
return (
<div>
<button onClick={reset}>Reset</button>
</div>
);
};
const App = () => (
<div className="App">
<ElapsedTime />
<Duration />
<Reset />
</div>
);
export default App;
Task 5: CRUD
아마 이 부분이 저한테 가장 도움이 되겠다고 생각했습니다.
프로젝트를 진행할 때에도 사실 가장 많이 쓰이는게 CRUD 기능이니까요
이 예제는 좀 더 곱씹어 보면서 따라 해봤던 것 같아요.
Atom 정의
import { atom, PrimitiveAtom } from "jotai";
export const nameAtom = atom("");
export const surnameAtom = atom("");
type NameItem = { name: string; surname: string };
export type NameItemAtom = PrimitiveAtom<NameItem>;
// 생성된 이름을 담을 List Atom
const nameListAtom = atom<NameItemAtom[]>([]);
// List에서 선택된 이름 Atom
const baseSelectedAtom = atom<NameItemAtom | null>(null);
// 리스트 중 선택된 Atom을 반환, 그리고 nameItemAtom을 파라미터로 받아, baseSelectedAtom에 저장시킵니다.
export const selectedAtom = atom(
(get) => get(baseSelectedAtom),
(get, set, nameItemAtom: NameItemAtom | null) => {
set(baseSelectedAtom, nameItemAtom);
// nameItemAtom 객체의 이름과 성을 각각 nameAtom, surnameAtom에 저장시킵니다.
if (nameItemAtom) {
const { name, surname } = get(nameItemAtom);
set(nameAtom, name);
set(surnameAtom, surname);
}
}
);
// 성(surname) 검색을 위한 Atom
export const prefixAtom = atom("");
// 검색 결과를 반환하는 Atom
export const filteredNameListAtom = atom((get) => {
// 검색어와 이름 목록들을 불러옵니다.
const prefix = get(prefixAtom);
const nameList = get(nameListAtom);
// 검색어가 없을 경우엔 이름 목록들을 전부 반환합니다.
if (!prefix) {
return nameList;
}
// 성(surname)을 기준으로 검색어 filter
return nameList.filter((nameItemAtom) =>
get(nameItemAtom).surname.startsWith(prefix)
);
});
// 이름 생성 Atom
export const createAtom = atom(
// nameAtom과 surnameAtom이 있을 경우, true 반환
(get) => !!get(nameAtom) && !!get(surnameAtom),
(get, set) => {
// 입력한 name과 surname을 불러와 객체로 만들어서 nameListAtom에 추가
const name = get(nameAtom);
const surname = get(surnameAtom);
if (name && surname) {
const nameItemAtom: NameItemAtom = atom({ name, surname });
set(nameListAtom, (prev) => [...prev, nameItemAtom]);
// 빈 문자열로 초기화
set(nameAtom, "");
set(surnameAtom, "");
set(selectedAtom, null);
}
}
);
// 이름 업데이트 Atom
export const updateAtom = atom(
// 이름을 선택하면 selectedAtom에서 작성한 코드에 의해 nameAtom과 surnameAtom이 선택된 name과 surname이 됩니다.
(get) => !!get(nameAtom) && !!get(surnameAtom) && !!get(selectedAtom),
(get, set) => {
// 선택된 상태에서 name과 surname을 수정하고, Update clickEvent 시 set처리를 시킵니다.
const name = get(nameAtom);
const surname = get(surnameAtom);
const selected = get(selectedAtom);
if (name && surname && selected) {
set(selected, { name, surname });
}
}
);
// 이름 삭제 Atom
export const deleteAtom = atom(
// 이름을 클릭했으면 true 반환
(get) => !!get(selectedAtom),
(get, set) => {
// 선택된 이름을 불러오고, nameListAtom에서 해당 이름을 filter로 제거합니다.
const selected = get(selectedAtom);
if (selected) {
set(nameListAtom, (prev) => prev.filter((item) => item !== selected));
}
}
);
Atom 적용
import { useMemo } from "react";
import { atom, useAtom } from "jotai";
import {
NameItemAtom,
nameAtom,
surnameAtom,
prefixAtom,
filteredNameListAtom,
selectedAtom,
createAtom,
updateAtom,
deleteAtom
} from "./atoms";
const Filter = () => {
// 검색을 위한 상태
const [prefix, setPrefix] = useAtom(prefixAtom);
return (
<div>
<span>Filter prefix:</span>
<input value={prefix} onChange={(e) => setPrefix(e.target.value)} />
</div>
);
};
const Item = ({ itemAtom }: { itemAtom: NameItemAtom }) => {
// 이름과 성을 담은 itemAtom 상태
const [{ name, surname }] = useAtom(itemAtom);
// 선택된 이름 상태, useMemo 훅을 사용하여 최적화 진행
const [selected, setSelected] = useAtom(
useMemo(
() =>
atom(
(get) => get(selectedAtom) === itemAtom,
(_get, set) => set(selectedAtom, itemAtom)
),
[itemAtom]
)
);
return (
<div
style={{
padding: "0.1em",
backgroundColor: selected ? "lightgray" : "transparent"
}}
onClick={setSelected}
>
{name}, {surname}
</div>
);
};
const List = () => {
// 검색까지 처리된 이름 리스트 상태
const [list] = useAtom(filteredNameListAtom);
return (
<div
style={{
width: "100%",
height: "8em",
overflow: "scroll",
border: "2px solid gray"
}}
>
// 이름 리스트들을 Item 컴포넌트를 통해 목록 생성
{list.map((item) => (
<Item key={String(item)} itemAtom={item} />
))}
</div>
);
};
const NameField = () => {
// 이름 상태
const [name, setName] = useAtom(nameAtom);
return (
<div>
<span>Name:</span>
<input value={name} onChange={(e) => setName(e.target.value)} />
</div>
);
};
const SurnameField = () => {
// 성 상태
const [surname, setSurname] = useAtom(surnameAtom);
return (
<div>
<span>Surname:</span>
<input value={surname} onChange={(e) => setSurname(e.target.value)} />
</div>
);
};
const CreateButton = () => {
// 이름 생성을 위한 Atom, 첫번째 get에는 name과 surname 유무에 의한 enabled, set 시키기 위한 create
const [enabled, create] = useAtom(createAtom);
return (
<button disabled={!enabled} onClick={create}>
Create
</button>
);
};
const UpdateButton = () => {
// 이름 업데이트를 위한 Atom, 첫번째 get에는 name, surname, selected 유무에 의한 enabled, set 시키기 위한 update
const [enabled, update] = useAtom(updateAtom);
return (
<button disabled={!enabled} onClick={update}>
Update
</button>
);
};
const DeleteButton = () => {
// 이름 삭제를 위한 Atom, 첫번째 get에는 name, surname, selected 유무에 의한 enabled, set 시키기 위한 del
const [enabled, del] = useAtom(deleteAtom);
return (
<button disabled={!enabled} onClick={del}>
Delete
</button>
);
};
const App = () => (
<div className="App">
<div style={{ display: "flex" }}>
<div style={{ width: "45%" }}>
<Filter />
<List />
</div>
<div style={{ width: "45%", margin: "auto" }}>
<NameField />
<SurnameField />
</div>
</div>
<CreateButton />
<UpdateButton />
<DeleteButton />
</div>
);
export default App;
ref : https://blog.axlight.com/posts/learning-react-state-manager-jotai-with-7guis-tasks/
댓글