이번 포스트는 jotai atom의 의미와 derived atom들의 종류와 기본적인 사용법을 알고있다는 가정 하에 작성 된 글입니다. 위 내용에 대해 익숙하지 않으시다면 이전 Jotai tutorial 시리즈를 먼저 읽고 오시는 것을 추천드립니다.
이번 포스트의 내용은 jotai를 개발한 Daishi Kato의 egghead lesson을 바탕으로 하고 있으며 전체 코드는 code sandbox에서 확인하실 수 있습니다.
지난 포스트로 jotai의 기초는 끝났다고 볼 수 있겠다. 이번 포스트 부터는 보다 실전적이거나 테크닉이 들어간 jotai 활용법을 알아 보자.
튜토리얼 시리즈에서 우리는 SVG 캔버스에 dot을 찍는 app을 하나의 파일에 모두 작성했다.
이제 튜토리얼을 졸업했으니 이 코드들을 나누어서 구조화 해보자.
요즘 핫한 Chat GPT에게 물어봤는데 생각보다 더 설명을 그럴듯하게 해주었다.
...
...
뭔가 더 적으려다 GPT님 말을 반복하는 것 같아서 그냥 다음으로 넘어가겠다....
구조화에 여러가지 방법이 있지만 여기서는 기능 별 컴포넌트로 나누고 연관된 컴포넌트 파일에 atom을 정의하는 방식을 선택하겠다. 이렇게 하면 다른 컴포넌트에서 쓰이는 atom만 export할 수 있게 된다.
먼저 타입을 정의하는 types.ts를 만들자.
// types.ts
export type Point = readonly [number, number];
Point는 읽기 전용인 [number, number] tuple type이다.
// SvgDots.tsx
import { atom, useAtom } from "jotai";
import { Point } from "./types";
const dotsAtom = atom<readonly Point[]>([]);
export const addDotAtom = atom(
null,
(_get, set, update: Point) => {
set(dotsAtom, (prev) => [...prev, update]);
}
);
export const SvgDots = () => {
const [dots] = useAtom(dotsAtom);
return (
<g>
{dots.map(([x, y]) => (
<circle cx={x} cy={y} r="2" fill="#aaa" />
))}
</g>
);
};
튜토리얼 시리즈에서 만든 점을 찍는 SvgDots를 SvgDots.tsx로 분리시켰다.
dotsAtom은 SvgDots 내부에서만 쓰이므로 export하지 않았고 dotsAtom에서 파생된 addDotAtom은 export 시켜줬다.
// SvgRoot.tsx
import { atom, useAtom } from "jotai";
import { Point } from "./types";
import { addDotAtom, SvgDots } from "./SvgDots";
const drawingAtom = atom(false);
const handleMouseDownAtom = atom(
null,
(get, set) => {
set(drawingAtom, true);
}
);
const handleMouseUpAtom = atom(null, (get, set) => {
set(drawingAtom, false);
});
const handleMouseMoveAtom = atom(
null,
(get, set, update: Point) => {
if (get(drawingAtom)) {
set(addDotAtom, update);
}
}
);
export const SvgRoot = () => {
const [, handleMouseUp] = useAtom(handleMouseUpAtom);
const [, handleMouseDown] = useAtom(handleMouseDownAtom);
const [, handleMouseMove] = useAtom(handleMouseMoveAtom);
return (
<svg
width="200"
height="200"
viewBox="0 0 200 200"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={(e) => {
handleMouseMove([e.clientX, e.clientY]);
}}
>
<rect width="200" height="200" fill="#eee" />
<SvgDots />
</svg>
);
};
import { SvgRoot } from "./SvgRoot";
const App = () => (
<>
<SvgRoot />
</>
);
export default App;
튜토리얼에서 봤던 친구들이다. 기능 역시 동일하다.
이제 점은 볼만큼 봤으니 선을 만들어 보자.
그 역할은 새로운 SvgShape.tsx에게 맡기자.
// SvgShape.tsx
import { atom, useAtom } from "jotai";
import { Point } from "./types";
const pointsToPath = (points: readonly Point[]) => {
let d = "";
points.forEach((point) => {
if (d) {
d += ` L ${point[0]} ${point[1]}`;
} else {
d = `M ${point[0]} ${point[1]}`;
}
});
return d;
};
const shapeAtom = atom({ path: "" });
export const addShapeAtom = atom(
null,
(_get, set, update: readonly Point[]) => {
set(shapeAtom, { path: pointsToPath(update) });
}
);
export const SvgShape = () => {
const [shape] = useAtom(shapeAtom);
return (
<g>
<path
d={shape.path}
fill="none"
stroke="black"
strokeWidth="3"
/>
</g>
);
};
갑자기 이게 뭔가 당황하지 말고 찬찬히 살펴보자.
// primitive atom
const shapeAtom = atom({ path: "" });
먼저 선의 경로를 값으로 가지는 shapeAtom이다.
// Write only atom
export const addShapeAtom = atom(
null,
(_get, set, update: readonly Point[]) => {
set(shapeAtom, { path: pointsToPath(update) });
}
);
addShapeAtom은 Point들을 받아서 path로 변환시켜주는 atom이다.
pointsToPath(points)는 그냥 그 역할을 하는 함수인가보다 하고 넘어가자.
SvgShape 컴포넌트는 shapeAtom의 값으로 path를 그려주는 컴포넌트가 되시겠다.
// SvgDots.tsx
...
import { addShapeAtom } from "./SvgShape";
...
...
export const commitDotsAtom = atom(
null,
(get, set) => {
set(addShapeAtom, get(dotsAtom));
set(dotsAtom, []);
}
);
SvgDots에 commitDotsAtom을 추가했다.
이 Write only atom은 dotsAtom의 값을 addShapeAtom에 set 해주고 dotsAtom을 초기화 시킨다.
즉 dotsAtom의 point들을 path로 바꿔주고 point들을 지워준다는 것
그럼 이제 SvgRoot에 추가해보자.
import { SvgShape } from "./SvgShape";
import { addDotAtom, commitDotsAtom, SvgDots } from "./SvgDots";
/*
중략
*/
const handleMouseUpAtom = atom(null, (get, set) => {
set(drawingAtom, false);
set(commitDotsAtom, null);
});
/*
중략
*/
export const SvgRoot = () => {
const [, handleMouseUp] = useAtom(handleMouseUpAtom);
const [, handleMouseDown] = useAtom(handleMouseDownAtom);
const [, handleMouseMove] = useAtom(handleMouseMoveAtom);
return (
<svg
width="200"
height="200"
viewBox="0 0 200 200"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={(e) => {
handleMouseMove([e.clientX, e.clientY]);
}}
>
<rect width="200" height="200" fill="#eee" />
{/* SvgShape 추가 */}
<SvgShape />
<SvgDots />
</svg>
);
};
onMouseUp 되는 순간 point들이 path로 바뀌어야 하니까 해당 atom에 commitDotsAtom을 setter(write function)에 넣어주었다.
자, 이제 생각한 대로 동작 하는지 캔버스에 그려보자
https://egghead.io/lessons/react-structure-jotai-atoms-and-add-functionality-to-a-react-app
https://codesandbox.io/embed/jotai-tutorial-04-6fkir