본문 바로가기
React.js

Jotai - 7GUIs 튜토리얼 - 2

by Zih0 2021. 10. 16.

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/

댓글