본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 10. · 17 Views
Jotai 내부 아키텍처 완벽 분석
Jotai는 React를 위한 원자적 상태 관리 라이브러리입니다. 이 카드 뉴스에서는 Jotai의 내부 구조부터 데이터 흐름, 핵심 알고리즘까지 초급 개발자도 이해할 수 있도록 쉽게 풀어냅니다. 실무에서 바로 적용할 수 있는 인사이트를 얻어가세요.
목차
1. 전체 폴더 구조 이해
김개발 씨는 프로젝트에서 Jotai를 사용하고 있지만, 내부가 어떻게 돌아가는지 궁금했습니다. "이렇게 간단한 API로 복잡한 상태 관리가 가능하다니, 도대체 내부는 어떻게 생겼을까?" 선배 박시니어 씨는 웃으며 말했습니다.
"그럼 소스코드를 한번 열어볼까요?"
Jotai의 폴더 구조는 매우 간결하고 직관적입니다. 핵심 로직은 vanilla 폴더에 있으며, React 바인딩은 react 폴더에 분리되어 있습니다.
이런 구조 덕분에 Jotai는 React뿐만 아니라 다른 프레임워크에서도 사용할 수 있는 확장성을 가집니다. 마치 엔진과 차체를 분리해서 다양한 자동차를 만들 수 있는 것처럼 말이죠.
다음 코드를 살펴봅시다.
// Jotai 폴더 구조 개요
jotai/
├── src/
│ ├── vanilla/ // 핵심 상태 관리 로직 (프레임워크 독립적)
│ │ ├── store.ts // 스토어 구현
│ │ ├── atom.ts // Atom 생성 함수
│ │ └── utils/ // 유틸리티 함수들
│ ├── react/ // React 전용 바인딩
│ │ ├── useAtom.ts // React Hook 구현
│ │ └── Provider.ts // Context Provider
│ └── index.ts // 공개 API 진입점
김개발 씨는 회사에서 Jotai를 사용한 지 벌써 두 달째입니다. 처음에는 단순히 "가벼운 상태 관리 라이브러리"라고만 생각했는데, 사용하면 할수록 신기한 점이 많았습니다.
Redux처럼 복잡한 설정도 없고, Context API처럼 리렌더링 걱정도 없습니다. 어느 날 팀 회의에서 후배 이주니어 씨가 물었습니다.
"선배님, Jotai가 어떻게 이렇게 간단한 API로 복잡한 기능을 제공하나요?" 김개발 씨도 궁금했던 부분이었습니다. 박시니어 씨가 화면을 공유하며 설명을 시작했습니다.
"좋은 질문이네요. Jotai의 비밀은 바로 폴더 구조에서 시작됩니다." 폴더 구조가 왜 중요할까요? 라이브러리의 폴더 구조는 단순히 파일을 정리하는 것 이상의 의미를 가집니다.
그것은 설계 철학을 보여줍니다. Jotai의 경우, 가장 중요한 특징은 vanilla와 react의 분리입니다.
마치 레고 블록과 같습니다. 기본 블록(vanilla)은 어떤 구조물에도 사용할 수 있지만, 특정 구조물(React 앱)을 만들 때는 전용 연결 부품(react)이 필요한 것처럼 말이죠.
vanilla 폴더의 역할 vanilla 폴더에는 Jotai의 핵심 엔진이 들어있습니다. 여기에는 React와 전혀 관계없는 순수한 상태 관리 로직만 존재합니다.
store.ts는 상태를 저장하고 관리하는 저장소를 구현하고, atom.ts는 상태의 최소 단위인 Atom을 생성합니다. 이렇게 분리한 이유는 무엇일까요?
바로 확장성 때문입니다. 만약 Vue나 Svelte에서 Jotai를 사용하고 싶다면, vanilla 폴더는 그대로 두고 vue나 svelte 폴더만 새로 만들면 됩니다.
핵심 로직을 재사용할 수 있는 거죠. react 폴더의 역할 react 폴더에는 React 전용 기능들이 모여있습니다.
useAtom, useAtomValue 같은 Hook들이 여기에 구현되어 있습니다. 이들은 vanilla의 기능을 React의 생명주기와 연결해주는 다리 역할을 합니다.
예를 들어 useAtom Hook은 내부적으로 vanilla의 store를 사용하지만, React의 useState와 useEffect를 활용해 컴포넌트 리렌더링을 트리거합니다. 마치 통역사가 두 언어를 연결해주는 것처럼, react 폴더는 vanilla와 React를 연결합니다.
utils 폴더의 숨은 보석들 utils 폴더에는 편의 기능들이 들어있습니다. atomWithStorage, atomFamily 같은 유틸리티 함수들이 여기 있죠.
이들은 기본 atom을 확장해서 더 편리하게 사용할 수 있게 해줍니다. 김개발 씨가 자주 사용하는 atomWithStorage는 localStorage와 연동되는 atom을 쉽게 만들어줍니다.
"아, 그래서 이렇게 간단하게 localStorage 연동이 되는구나!" 김개발 씨는 이제야 이해가 되었습니다. index.ts의 중요성 index.ts는 공개 API의 진입점입니다.
개발자가 "import { atom } from 'jotai'"라고 쓸 때, 실제로는 이 파일을 거쳐갑니다. 여기서 vanilla와 react의 기능들을 선별해서 외부에 노출합니다.
마치 식당의 메뉴판 같습니다. 주방(vanilla, react)에는 수많은 재료와 조리법이 있지만, 손님(개발자)에게는 깔끔하게 정리된 메뉴만 보여주는 것이죠.
실제 프로젝트에서의 활용 박시니어 씨는 실제 경험을 공유했습니다. "작년에 React Native 프로젝트를 진행할 때, Jotai를 선택한 이유가 바로 이 구조 때문이었어요.
vanilla 로직은 그대로 사용하고, react-native용 바인딩만 약간 수정하면 됐거든요." 이주니어 씨가 눈을 빛냈습니다. "그럼 이론적으로는 어떤 프레임워크에서도 사용할 수 있다는 거네요?" "정확합니다.
그게 바로 좋은 아키텍처의 힘이죠." 정리하며 김개발 씨는 노트북을 닫으며 생각했습니다. "단순히 라이브러리를 사용하는 것과 내부를 이해하는 것은 천지 차이구나." 폴더 구조 하나에도 이렇게 깊은 설계 철학이 담겨있다니, 앞으로 다른 라이브러리를 볼 때도 폴더 구조부터 살펴봐야겠다고 다짐했습니다.
Jotai의 폴더 구조는 간결하지만 강력합니다. vanilla와 react의 분리는 확장성을 제공하고, 명확한 책임 분리는 유지보수를 쉽게 만듭니다.
여러분도 다음에 라이브러리를 선택할 때, 폴더 구조를 한번 살펴보세요. 그 안에 설계자의 철학이 담겨있을 겁니다.
실전 팁
💡 - 라이브러리를 학습할 때는 폴더 구조부터 파악하면 전체 그림이 보입니다
- vanilla와 framework 바인딩의 분리는 좋은 아키텍처의 신호입니다
- index.ts를 보면 어떤 API가 공개되어 있는지 한눈에 파악할 수 있습니다
2. 엔트리 포인트 분석
드디어 코드를 열어볼 시간입니다. 김개발 씨는 GitHub에서 Jotai 저장소를 클론했습니다.
"가장 먼저 어디를 봐야 할까요?" 박시니어 씨가 답했습니다. "라이브러리를 이해하려면 진입점부터 봐야죠.
src/vanilla/atom.ts를 열어보세요."
atom 함수는 Jotai의 가장 기본이 되는 함수입니다. 이 함수는 상태의 최소 단위를 생성하는 팩토리 함수로, 내부적으로는 단순히 설정 객체를 반환합니다.
실제 상태는 여기에 저장되지 않으며, atom은 설계도 역할만 합니다. 마치 집의 설계도면이 집 자체는 아니지만, 집을 짓는 데 필요한 모든 정보를 담고 있는 것과 같습니다.
다음 코드를 살펴봅시다.
// src/vanilla/atom.ts 핵심 구현
export function atom<Value>(initialValue: Value) {
// 고유한 키 생성 (디버깅용)
const key = `atom${++keyCount}`
// Atom 설정 객체 반환
const config = {
toString: () => key,
read: (get) => get(config),
write: (get, set, update) => set(config, update),
init: initialValue,
}
return config
}
김개발 씨가 코드를 열자마자 "어? 이게 전부인가요?"라고 놀라며 말했습니다.
atom 함수는 놀라울 정도로 단순했습니다. 겨우 몇 줄의 코드로 상태 관리의 기초를 만들고 있었던 것입니다.
박시니어 씨는 웃으며 설명을 시작했습니다. "네, 이게 전부입니다.
Jotai의 아름다움은 바로 이 단순함에 있어요." atom은 무엇을 반환할까요? 많은 개발자들이 오해하는 부분이 있습니다. atom을 호출하면 상태가 생성될 것 같지만, 실제로는 설정 객체만 반환됩니다.
상태 자체는 아직 만들어지지 않았습니다. 마치 레시피를 작성하는 것과 같습니다.
"계란 2개, 우유 100ml"라고 레시피를 쓴다고 해서 팬케이크가 만들어지는 건 아니죠. 실제로 요리를 하려면 주방(store)이 필요합니다.
config 객체의 구조 반환되는 config 객체는 네 가지 주요 속성을 가집니다. 첫째, toString은 디버깅용 키를 반환합니다.
개발자 도구에서 "atom1", "atom2"처럼 표시되는 게 바로 이것입니다. 둘째, read 함수는 값을 읽는 방법을 정의합니다.
기본적으로는 자기 자신의 값을 반환하지만, derived atom의 경우 다른 atom들을 조합할 수 있습니다. 셋째, write 함수는 값을 쓰는 방법을 정의합니다.
단순히 값을 업데이트하거나, 복잡한 로직을 실행할 수도 있습니다. 마지막으로 init은 초기값입니다.
이 값은 store가 처음 atom을 초기화할 때 사용됩니다. 왜 이렇게 단순할까요? 김개발 씨가 궁금해했습니다.
"Redux는 이렇게 간단하지 않았는데, Jotai는 왜 이렇게 단순한가요?" 박시니어 씨는 화이트보드에 그림을 그리며 설명했습니다. "Redux는 모든 상태를 하나의 거대한 객체로 관리합니다.
그러다 보니 액션, 리듀서, 미들웨어 등 많은 개념이 필요했죠. 하지만 Jotai는 상태를 원자 단위로 쪼갭니다.
각 atom은 독립적이고, 필요할 때만 조합하면 됩니다." 이는 마치 거대한 창고 하나를 관리하는 것과, 작은 서랍장 여러 개를 관리하는 것의 차이입니다. 서랍장은 각각 독립적이라 관리가 쉽습니다.
read와 write의 비밀 기본 atom의 read와 write는 매우 단순합니다. read는 "get(config)"로 자기 자신의 값을 읽고, write는 "set(config, update)"로 새 값을 씁니다.
하지만 이게 전부가 아닙니다. derived atom을 만들 때는 이 함수들을 커스터마이징할 수 있습니다.
예를 들어 두 atom의 합을 반환하는 atom을 만들 수도 있고, 값을 쓸 때 특별한 로직을 실행할 수도 있습니다. 실제 사용 예시 김개발 씨는 실제 프로젝트 코드를 떠올렸습니다.
"const countAtom = atom(0)"이라고 쓸 때, 실제로는 설정 객체가 생성되는 거군요. 그럼 실제 숫자 0은 어디에 저장되나요?" "좋은 질문입니다!" 박시니어 씨가 다음 파일을 열었습니다.
"바로 store에 저장됩니다. atom은 설계도이고, store는 실제 값을 보관하는 창고죠.
이건 다음에 자세히 볼 거예요." keyCount의 역할 코드를 자세히 보면 keyCount라는 변수가 있습니다. 이건 모듈 레벨에서 관리되는 카운터로, 각 atom에 고유한 번호를 부여합니다.
"atom1", "atom2", "atom3" 이런 식으로 말이죠. 이게 왜 필요할까요?
디버깅할 때 어떤 atom이 문제인지 식별하기 위해서입니다. 개발자 도구에서 "atom47이 예상치 못한 값을 가지고 있습니다"라는 메시지를 보면, 코드에서 해당 atom을 찾기 쉬워집니다.
정리하며 김개발 씨는 이제 atom의 정체를 확실히 이해했습니다. "결국 atom은 그냥 설정 객체였구나.
실제 마법은 다른 곳에 있겠네요." 박시니어 씨가 고개를 끄덕였습니다. "정확합니다.
이제 store를 볼 차례입니다." atom 함수의 단순함은 Jotai 철학의 핵심입니다. 복잡한 기능은 조합을 통해 만들고, 기본은 최대한 단순하게 유지합니다.
이것이 바로 최소주의 설계의 힘입니다.
실전 팁
💡 - atom은 설정 객체를 반환하며, 실제 상태는 store에 저장됩니다
- read와 write를 커스터마이징하면 다양한 패턴을 구현할 수 있습니다
- toString 메서드는 디버깅 시 atom을 식별하는 데 유용합니다
3. 핵심 모듈 관계도
박시니어 씨가 화이트보드 앞에 섰습니다. "자, 이제 퍼즐 조각들을 맞춰볼 시간입니다.
atom, store, 그리고 useAtom이 어떻게 협력하는지 보여드릴게요." 김개발 씨와 이주니어 씨는 자리에서 몸을 앞으로 기울였습니다.
Jotai의 핵심은 세 가지 모듈의 협력입니다. atom은 상태 설계도를 제공하고, store는 실제 상태를 저장하며, useAtom은 React와 연결합니다.
이들의 관계는 명확한 책임 분리를 보여주며, 각자의 역할에만 집중합니다. 마치 오케스트라에서 각 악기가 자신의 파트만 연주하지만, 함께 조화로운 음악을 만드는 것과 같습니다.
다음 코드를 살펴봅시다.
// 모듈 간 관계를 보여주는 개념 코드
// 1. atom: 설계도 생성
const countAtom = atom(0)
// 2. store: 실제 값 저장 및 관리
const store = createStore()
store.set(countAtom, 5) // atom의 값을 5로 설정
const value = store.get(countAtom) // 값 읽기
// 3. useAtom: React와 연결
function Counter() {
// useAtom이 내부적으로 store를 사용
const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
박시니어 씨가 화이트보드에 세 개의 원을 그렸습니다. "Jotai는 이 세 가지가 톱니바퀴처럼 맞물려 돌아갑니다." 첫 번째 원에 "atom"이라고 썼습니다.
atom의 역할: 설계자 "atom은 설계자입니다. 건축으로 치면 건축가가 도면을 그리는 것과 같죠." 김개발 씨가 끄덕였습니다.
"그럼 실제 건물은요?" "바로 store가 건물을 짓습니다." atom은 어떤 데이터를 저장할지, 어떻게 읽고 쓸지만 정의합니다. 실제로 데이터를 가지고 있지는 않습니다.
이런 분리 덕분에 같은 atom을 여러 곳에서 재사용할 수 있습니다. store의 역할: 창고지기 두 번째 원에 "store"라고 쓰며 박시니어 씨가 설명을 이어갔습니다.
"store는 창고지기입니다. 모든 atom의 실제 값을 보관하고, 누가 그 값을 구독하고 있는지 추적합니다." store는 내부적으로 WeakMap을 사용해 atom과 값을 매핑합니다.
예를 들어 countAtom의 값이 5라면, store 내부에는 "countAtom → 5"라는 매핑이 저장됩니다. 더 중요한 것은 의존성 추적입니다.
만약 atom A가 atom B를 읽는다면, store는 "B가 변경되면 A도 다시 계산해야 한다"는 것을 기억합니다. 마치 스프레드시트에서 수식이 참조하는 셀을 추적하는 것처럼 말이죠.
useAtom의 역할: 통역사 세 번째 원에 "useAtom"이라고 쓰며 마지막 설명을 시작했습니다. "useAtom은 통역사입니다.
store의 언어를 React의 언어로 번역해줍니다." React 컴포넌트는 store를 직접 다룰 수 없습니다. useState나 useReducer 같은 Hook을 통해서만 상태를 관리할 수 있죠.
useAtom은 이 둘을 연결합니다. 내부적으로 useAtom은 store.get()으로 값을 읽고, store.set()으로 값을 씁니다.
그리고 React의 useState를 활용해 값이 변경되면 컴포넌트를 리렌더링합니다. 협력의 과정 이주니어 씨가 질문했습니다.
"그럼 실제로 컴포넌트에서 값을 읽을 때는 어떤 일이 일어나나요?" 박시니어 씨가 화살표를 그리며 답했습니다. "좋은 질문입니다.
단계별로 보면 이렇습니다. 첫째, useAtom이 호출되면 내부적으로 store.get(atom)을 실행합니다.
둘째, store는 atom의 read 함수를 실행해서 값을 계산합니다. 셋째, 그 값이 useAtom을 거쳐 컴포넌트로 반환됩니다." 김개발 씨가 끼어들었습니다.
"그럼 값을 변경할 때는요?" "반대 방향입니다. setCount가 호출되면 useAtom이 store.set(atom, newValue)를 실행하고, store는 atom의 write 함수를 실행합니다.
그리고 해당 atom을 구독하는 모든 컴포넌트에게 알립니다." 실제 동작 시나리오 박시니어 씨가 구체적인 예시를 들었습니다. "쇼핑몰 장바구니를 생각해봅시다.
cartAtom이 있고, 그걸 CartIcon과 CartPage에서 사용한다고 가정하죠." 사용자가 상품을 추가하면, CartPage의 버튼이 setCart를 호출합니다. 이게 store로 전달되고, store는 cartAtom의 값을 업데이트합니다.
그 순간 store는 "cartAtom을 구독하는 컴포넌트가 누구지?"를 확인하고, CartIcon과 CartPage 모두에게 알립니다. 두 컴포넌트는 리렌더링되어 새로운 장바구니 개수를 표시합니다.
Provider의 역할 "그런데 store는 어디서 오나요?" 김개발 씨가 또 질문했습니다. 박시니어 씨가 웃으며 답했습니다.
"기본적으로는 전역 store가 자동으로 생성됩니다. 하지만 Provider를 사용하면 커스텀 store를 주입할 수 있죠." 이는 테스트할 때 특히 유용합니다.
각 테스트마다 독립적인 store를 만들어 사용하면, 테스트 간 간섭이 없어집니다. 또한 서버 사이드 렌더링에서도 각 요청마다 별도의 store를 사용할 수 있습니다.
의존성 그래프 박시니어 씨가 추가로 그림을 그렸습니다. "atom끼리도 의존 관계를 만들 수 있습니다.
예를 들어 fullNameAtom이 firstNameAtom과 lastNameAtom을 조합한다면, store는 이 관계를 그래프로 관리합니다." 이 그래프 덕분에 효율적인 업데이트가 가능합니다. firstNameAtom이 변경되면, fullNameAtom만 다시 계산하면 되고, 관계없는 다른 atom들은 그대로 둡니다.
정리하며 김개발 씨는 화이트보드를 사진으로 찍었습니다. "이제 전체 그림이 보이네요.
atom, store, useAtom이 각자 맡은 역할에만 집중하면서도, 완벽하게 협력하는구나." Jotai의 아름다움은 바로 이 명확한 책임 분리에 있습니다. 각 모듈은 하나의 역할만 하지만, 함께 모이면 강력한 상태 관리 시스템이 됩니다.
이것이 바로 좋은 소프트웨어 설계의 본보기입니다.
실전 팁
💡 - atom은 설계도, store는 실제 저장소, useAtom은 React 브릿지 역할을 합니다
- store는 WeakMap으로 atom-value 매핑을 관리하며 의존성 그래프를 추적합니다
- Provider를 사용하면 테스트나 SSR을 위한 독립적인 store를 만들 수 있습니다
4. 데이터 흐름 추적
이주니어 씨가 버그를 만났습니다. "이상해요, 분명히 값을 업데이트했는데 화면이 안 바뀌어요." 박시니어 씨가 코드를 보더니 말했습니다.
"Jotai의 데이터 흐름을 이해하면 이런 문제를 쉽게 해결할 수 있어요. 값이 어떻게 흘러가는지 따라가 봅시다."
Jotai의 데이터 흐름은 단방향으로 명확합니다. 값의 변경은 항상 store를 거치며, 구독자에게 전파됩니다.
이 과정에서 의존성 추적, 배칭, 그리고 선택적 리렌더링이 자동으로 처리됩니다. 마치 강물이 상류에서 하류로 흐르듯, 데이터도 예측 가능한 경로를 따라 흐릅니다.
다음 코드를 살펴봅시다.
// 데이터 흐름 예제
const nameAtom = atom('철수')
const greetingAtom = atom((get) => `안녕하세요, ${get(nameAtom)}님!`)
function Profile() {
const [name, setName] = useAtom(nameAtom)
const greeting = useAtomValue(greetingAtom)
const handleClick = () => {
// 1. setName 호출 → store.set 실행
setName('영희')
// 2. nameAtom 업데이트
// 3. greetingAtom이 nameAtom에 의존하므로 자동 재계산
// 4. 두 값을 구독하는 Profile 컴포넌트 리렌더링
}
return <div onClick={handleClick}>{greeting}</div>
}
이주니어 씨의 버그는 간단했습니다. 값을 직접 수정하려고 했던 것이죠.
"cartItems.push(newItem)"처럼 말이에요. 하지만 Jotai에서는 이렇게 하면 안 됩니다.
반드시 setter를 통해야 합니다. 박시니어 씨가 화면을 공유하며 설명을 시작했습니다.
"Jotai의 데이터 흐름은 마치 수도관 시스템과 같습니다. 물(데이터)은 정해진 파이프(store)를 통해서만 흐르고, 밸브(setter)를 열어야 흐름이 시작됩니다." 읽기 흐름: get의 여정 먼저 값을 읽는 과정을 살펴봅시다.
컴포넌트에서 useAtom이나 useAtomValue를 호출하면 무슨 일이 일어날까요? 첫 단계는 store에 접근입니다.
useAtom은 내부적으로 현재 store를 가져옵니다. 기본값을 사용하거나, Provider로 주입된 store를 사용하죠.
두 번째 단계는 atom의 read 실행입니다. store는 atom의 read 함수를 호출하면서 특별한 get 함수를 전달합니다.
이 get 함수는 다른 atom을 읽을 수 있게 해주는 동시에, 의존성을 추적합니다. 예를 들어 greetingAtom의 read에서 get(nameAtom)을 호출하면, store는 "greetingAtom이 nameAtom에 의존한다"는 정보를 기록합니다.
마치 스프레드시트가 "셀 C1이 A1과 B1을 참조한다"는 걸 기억하는 것과 같습니다. 세 번째 단계는 구독 등록입니다.
useAtom은 해당 atom의 값이 변경되면 컴포넌트를 리렌더링해야 한다고 store에 알립니다. store는 내부 구독자 목록에 이 컴포넌트를 추가합니다.
쓰기 흐름: set의 여정 이제 값을 쓰는 과정입니다. setName('영희')를 호출하면 어떻게 될까요?
첫 번째로, setter가 store.set을 호출합니다. 이때 업데이트할 atom과 새 값을 전달합니다.
중요한 점은 직접 값을 변경하는 게 아니라, store를 통한다는 것입니다. 두 번째로, atom의 write 함수가 실행됩니다.
기본 atom의 경우 단순히 새 값을 저장하지만, 커스텀 write를 정의했다면 복잡한 로직이 실행될 수 있습니다. 예를 들어 여러 atom을 동시에 업데이트하거나, API 호출을 할 수도 있습니다.
세 번째로, 의존성 체인이 업데이트됩니다. nameAtom이 변경되었으니, 이것에 의존하는 greetingAtom도 무효화됩니다.
store는 "greetingAtom의 값이 오래되었으니 다음에 읽을 때 다시 계산해야 한다"고 표시합니다. 네 번째로, 구독자에게 알림이 전송됩니다.
nameAtom과 greetingAtom을 구독하는 모든 컴포넌트에게 "값이 바뀌었으니 리렌더링하세요"라고 알립니다. 하지만 관계없는 다른 컴포넌트는 영향을 받지 않습니다.
배칭의 마법 김개발 씨가 질문했습니다. "만약 여러 atom을 한꺼번에 업데이트하면 어떻게 되나요?
리렌더링이 여러 번 일어나나요?" 박시니어 씨가 웃으며 답했습니다. "좋은 질문입니다.
걱정하지 마세요. Jotai는 React의 배칭을 활용합니다.
같은 이벤트 핸들러 안에서 여러 atom을 업데이트해도, 리렌더링은 한 번만 일어납니다." 이는 마치 우편 배달과 같습니다. 편지가 도착할 때마다 집에 가는 게 아니라, 여러 편지를 모아서 한 번에 배달하는 것이죠.
의존성 그래프의 효율성 이주니어 씨가 또 질문했습니다. "atom이 엄청 많으면 느려지지 않나요?" 박시니어 씨가 화이트보드에 그래프를 그렸습니다.
"아닙니다. 의존성 그래프 덕분에 필요한 부분만 업데이트됩니다.
예를 들어 앱에 100개의 atom이 있고, 그중 하나가 변경되었다고 합시다. 그럼 그 atom에 직접 의존하는 atom들만 재계산됩니다.
나머지 95개는 그대로죠." 이는 선택적 리렌더링의 핵심입니다. Redux에서는 전체 상태 객체가 바뀌면 모든 연결된 컴포넌트를 확인해야 했습니다.
하지만 Jotai는 정확히 영향받는 부분만 찾아갑니다. 실제 버그 수정 박시니어 씨가 이주니어 씨의 코드를 고쳤습니다.
"문제는 여기예요. 배열을 직접 수정하면 참조가 그대로라 변경을 감지하지 못합니다.
새로운 배열을 만들어야 해요." 잘못된 코드: setCart(cart => { cart.push(newItem) return cart }) 올바른 코드: setCart(cart => [...cart, newItem]) "아!" 이주니어 씨가 무릎을 쳤습니다. "새로운 참조를 만들어야 변경을 감지하는구나!" "정확합니다.
이건 React의 불변성 원칙과 같아요." 디버깅 팁 김개발 씨가 노트에 적었습니다. "그럼 데이터 흐름을 디버깅하려면 어떻게 해야 하나요?" 박시니어 씨가 Chrome 개발자 도구를 열었습니다.
"Jotai DevTools를 사용하면 모든 atom의 값과 의존성을 시각적으로 볼 수 있어요. 어떤 atom이 언제 업데이트되었는지, 어떤 컴포넌트가 구독하고 있는지 한눈에 파악할 수 있죠." 정리하며 이주니어 씨는 자신의 버그를 고치며 배운 점을 정리했습니다.
"데이터 흐름을 이해하니까 디버깅이 쉬워지네요. 값이 어떻게 흘러가는지 알면, 문제가 어디서 생겼는지 추적할 수 있어요." Jotai의 데이터 흐름은 단순하지만 강력합니다.
단방향 흐름, 자동 의존성 추적, 효율적인 배칭이 모두 자동으로 처리됩니다. 개발자는 비즈니스 로직에만 집중하면 됩니다.
실전 팁
💡 - 값은 반드시 setter를 통해 업데이트해야 하며, 직접 수정하면 감지되지 않습니다
- 의존성 추적은 자동으로 이루어지며, get 함수 호출 시 기록됩니다
- Jotai DevTools를 사용하면 데이터 흐름을 시각적으로 디버깅할 수 있습니다
5. 주요 알고리즘 분석
김개발 씨는 이제 Jotai가 어떻게 작동하는지 대략 이해했습니다. 하지만 한 가지 더 궁금한 게 있었습니다.
"이렇게 빠른 성능은 어떻게 가능한 거죠?" 박시니어 씨가 미소지었습니다. "비밀은 알고리즘에 있습니다.
특히 의존성 추적과 메모이제이션이 핵심이죠."
Jotai의 성능은 두 가지 핵심 알고리즘에서 나옵니다. 첫째는 의존성 추적 알고리즘으로, 어떤 atom이 어떤 atom을 참조하는지 자동으로 파악합니다.
둘째는 메모이제이션 알고리즘으로, 의존성이 변하지 않으면 재계산을 건너뜁니다. 이 두 알고리즘이 협력하여 최소한의 계산만 수행합니다.
다음 코드를 살펴봅시다.
// 의존성 추적 알고리즘 개념 코드
function trackDependencies<Value>(atom: Atom<Value>, store: Store): Value {
// 현재 추적 중인 의존성 집합
const dependencies = new Set<Atom>()
// 커스텀 get 함수: 다른 atom을 읽을 때마다 의존성 기록
const trackedGet = <V>(targetAtom: Atom<V>): V => {
dependencies.add(targetAtom) // 의존성 추가
return store.get(targetAtom) // 실제 값 반환
}
// atom의 read 함수 실행
const value = atom.read(trackedGet)
// 의존성 관계를 store에 저장
store.saveDependencies(atom, dependencies)
return value
}
박시니어 씨가 코드 에디터를 열었습니다. "자, 이제 Jotai의 심장부를 들여다볼 시간입니다.
복잡해 보이지만, 핵심 아이디어는 단순합니다." 의존성 추적: 누가 누구를 보고 있나? 김개발 씨가 커피를 한 모금 마시며 집중했습니다. "의존성 추적이 정확히 뭔가요?" "좋은 질문입니다.
예를 들어 fullNameAtom이 firstNameAtom과 lastNameAtom을 읽는다고 해봅시다. 그럼 store는 'fullNameAtom은 firstNameAtom과 lastNameAtom에 의존한다'는 정보를 어딘가에 저장해야 합니다.
이게 의존성 추적이죠." 문제는 개발자가 명시적으로 의존성을 선언하지 않는다는 것입니다. Redux에서는 useSelector의 두 번째 인자로 의존성 배열을 전달했지만, Jotai는 그럴 필요가 없습니다.
어떻게 가능할까요? trackedGet의 비밀 "핵심은 바로 trackedGet 함수입니다." 박시니어 씨가 위의 코드를 가리켰습니다.
"atom의 read 함수가 실행될 때, 일반 get이 아니라 특별한 trackedGet을 전달합니다." trackedGet은 무엇을 할까요? 다른 atom을 읽을 때마다, 그 정보를 Set에 기록합니다.
"아, 너 이 atom을 읽었구나? 그럼 너는 이 atom에 의존하는 거야"라고 말이죠.
이는 마치 도서관에서 책을 빌릴 때 대출 기록이 남는 것과 같습니다. 나중에 그 책이 반납되면(값이 변경되면), 도서관은 누가 그 책을 빌렸는지(어떤 atom이 의존하는지) 확인할 수 있습니다.
실행 시점의 추적 중요한 점은 실행 시점에 추적한다는 것입니다. 정적 분석으로는 불가능합니다.
왜냐하면 의존성이 동적으로 바뀔 수 있기 때문입니다. 예를 들어 이런 atom을 생각해봅시다: ``` const conditionalAtom = atom((get) => { const flag = get(flagAtom) return flag ?
get(atomA) : get(atomB) }) ``` flag가 true일 때는 atomA에 의존하지만, false일 때는 atomB에 의존합니다. 정적으로는 알 수 없고, 실행해봐야 알 수 있죠.
trackedGet이 이를 자동으로 처리합니다. 메모이제이션: 불필요한 계산 건너뛰기 이주니어 씨가 질문했습니다.
"의존성을 추적하는 건 알겠는데, 그걸 어떻게 활용하나요?" "바로 메모이제이션입니다." 박시니어 씨가 새로운 다이어그램을 그렸습니다. "atom의 값을 계산하면, store는 그 값을 캐시에 저장합니다.
그리고 의존하는 atom들도 함께 기록하죠." 다음에 같은 atom을 읽을 때, store는 먼저 확인합니다. "의존성들이 변했나?" 만약 모든 의존성의 값이 그대로라면, 캐시된 값을 바로 반환합니다.
재계산할 필요가 없는 거죠. WeakMap의 역할 김개발 씨가 코드를 자세히 보다가 물었습니다.
"WeakMap을 왜 사용하나요? 그냥 Map이면 안 되나요?" "예리한 질문입니다!" 박시니어 씨가 칭찬했습니다.
"WeakMap을 사용하면 메모리 누수를 방지할 수 있습니다. atom 객체가 더 이상 참조되지 않으면, WeakMap의 항목도 자동으로 가비지 컬렉션됩니다." 일반 Map을 사용하면, atom을 삭제해도 Map에 계속 남아있어 메모리가 낭비됩니다.
특히 동적으로 atom을 생성하고 삭제하는 경우, 이는 심각한 메모리 누수로 이어질 수 있습니다. 위상 정렬: 올바른 순서로 업데이트하기 "한 가지 더 복잡한 문제가 있습니다." 박시니어 씨가 화이트보드에 그래프를 그렸습니다.
"atom A가 B에 의존하고, B가 C에 의존한다면, 업데이트 순서가 중요합니다." C가 변경되면, B를 먼저 업데이트하고, 그다음에 A를 업데이트해야 합니다. 순서가 바뀌면 A가 오래된 B 값을 읽을 수 있죠.
Jotai는 내부적으로 위상 정렬 알고리즘을 사용합니다. 의존성 그래프를 순회하며, 의존하는 atom이 없는 것부터 차례대로 업데이트합니다.
이는 대학교 선수과목 시스템과 비슷합니다. 기초 과목을 먼저 듣고, 그다음에 심화 과목을 들어야 하는 것처럼 말이죠.
배칭과 큐 "마지막으로, 배칭 알고리즘입니다." 박시니어 씨가 설명을 이어갔습니다. "여러 atom이 동시에 업데이트되면, 알림을 즉시 보내지 않습니다.
대신 큐에 쌓아뒀다가, 한꺼번에 처리합니다." 이는 마치 식당 주방과 같습니다. 주문이 들어올 때마다 즉시 조리하는 게 아니라, 여러 주문을 모아서 효율적으로 조리하는 것이죠.
React 18의 자동 배칭과 함께 사용하면, 성능이 더욱 향상됩니다. 같은 렌더링 사이클 내의 모든 상태 업데이트가 하나로 합쳐집니다.
실제 성능 측정 김개발 씨가 궁금해했습니다. "실제로 얼마나 빠른가요?" 박시니어 씨가 벤치마크 결과를 보여줬습니다.
"간단한 테스트로, 1000개의 atom을 만들고 그중 하나를 업데이트하는 시간을 측정했어요. Jotai는 약 1ms가 걸렸고, 순진한 구현은 50ms 이상 걸렸습니다.
의존성 추적과 메모이제이션 덕분이죠." 정리하며 이주니어 씨가 감탄했습니다. "알고리즘이 이렇게 중요한 줄 몰랐어요.
겉보기엔 간단해 보이는데, 내부는 정말 정교하네요." 박시니어 씨가 고개를 끄덕였습니다. "좋은 라이브러리는 복잡함을 숨깁니다.
개발자는 간단한 API만 보지만, 내부에서는 최적화된 알고리즘이 열심히 일하고 있죠." Jotai의 알고리즘은 우아하면서도 실용적입니다. 의존성 추적, 메모이제이션, 위상 정렬, 배칭이 모두 조화롭게 작동하여, 개발자는 성능 걱정 없이 코드를 작성할 수 있습니다.
실전 팁
💡 - 의존성은 실행 시점에 자동으로 추적되며, 명시적 선언이 필요 없습니다
- WeakMap 사용으로 메모리 누수를 방지하고, 동적 atom 생성이 안전합니다
- 위상 정렬 덕분에 복잡한 의존성 그래프에서도 올바른 업데이트 순서가 보장됩니다
6. 설계 철학 이해하기
회의실에서 팀 회의가 열렸습니다. 주제는 "다음 프로젝트에서 어떤 상태 관리 라이브러리를 사용할 것인가"였습니다.
김개발 씨는 자신 있게 손을 들었습니다. "Jotai를 제안하고 싶습니다.
단순히 기능 때문이 아니라, 설계 철학이 우리 팀과 맞기 때문입니다." 모두가 김개발 씨를 주목했습니다.
Jotai의 설계 철학은 최소주의, 원자성, 확장성으로 요약됩니다. 핵심 API는 최소한으로 유지하되, 조합을 통해 복잡한 기능을 구현할 수 있습니다.
상태를 원자 단위로 쪼개어 독립적으로 관리하며, 필요할 때만 조합합니다. 그리고 vanilla 코어 덕분에 어떤 프레임워크에서도 사용할 수 있는 확장성을 가집니다.
다음 코드를 살펴봅시다.
// Jotai 설계 철학을 보여주는 예제
// 1. 최소주의: 기본 API는 단순
const countAtom = atom(0)
const doubleAtom = atom((get) => get(countAtom) * 2)
// 2. 원자성: 상태를 작은 단위로 분리
const firstNameAtom = atom('철수')
const lastNameAtom = atom('김')
const fullNameAtom = atom((get) =>
`${get(lastNameAtom)}${get(firstNameAtom)}`
)
// 3. 확장성: 조합으로 복잡한 패턴 구현
const asyncAtom = atom(async (get) => {
const id = get(userIdAtom)
const response = await fetch(`/api/users/${id}`)
return response.json()
})
김개발 씨가 프레젠테이션을 시작했습니다. "지난 몇 주간 Jotai의 내부를 공부하면서, 단순히 '또 다른 상태 관리 라이브러리'가 아니라는 걸 깨달았습니다.
Jotai는 명확한 철학을 가지고 있습니다." 최소주의: 덜어내는 용기 "첫 번째 철학은 최소주의입니다." 김개발 씨가 첫 슬라이드를 넘겼습니다. "Jotai의 핵심 API는 단 두 가지입니다.
atom과 useAtom. 이게 전부예요." 팀원 중 한 명이 의아해했습니다.
"너무 단순한 거 아닌가요? Redux는 훨씬 많은 기능이 있는데..." 김개발 씨가 웃으며 답했습니다.
"그게 포인트입니다. 많은 기능이 항상 좋은 건 아니에요." 최소주의의 핵심은 배우기 쉽다는 것입니다.
신입 개발자가 팀에 합류했을 때, Redux는 액션, 리듀서, 미들웨어, 셀렉터 등 배워야 할 게 많습니다. 하지만 Jotai는 atom과 useAtom만 알면 시작할 수 있습니다.
더 중요한 것은 유지보수입니다. 기능이 많으면 그만큼 버그가 생길 여지도 많고, 업데이트도 복잡해집니다.
Jotai는 핵심을 최소화하고, 나머지는 유틸리티로 분리했습니다. 원자성: 작은 것의 힘 "두 번째 철학은 원자성입니다." 김개발 씨가 화이트보드에 그림을 그렸습니다.
"Redux는 모든 상태를 하나의 거대한 트리로 관리합니다. 하지만 Jotai는 상태를 원자 단위로 쪼갭니다." 박시니어 씨가 끼어들었습니다.
"원자성의 장점을 실제 예시로 보여주면 어떨까요?" 김개발 씨가 고개를 끄덕이고 코드를 보여줬습니다. "쇼핑몰을 예로 들어봅시다.
Redux에서는 이렇게 했을 겁니다: ``` { user: { name, email, ... }, cart: { items, total, ...
}, products: { list, filter, ... } } ``` 모든 게 하나의 객체에 있죠.
cart를 업데이트하려면 전체 상태를 복사해야 합니다." "하지만 Jotai에서는: const userAtom = atom({ name, email }) const cartAtom = atom({ items, total }) const productsAtom = atom({ list, filter }) 각각 독립적입니다. cart를 업데이트해도 user나 products에는 전혀 영향이 없어요." 원자성의 진짜 힘은 선택적 리렌더링입니다.
cart를 사용하는 컴포넌트만 리렌더링되고, 나머지는 그대로입니다. Redux에서는 연결된 모든 컴포넌트를 확인해야 했지만, Jotai는 정확히 필요한 부분만 업데이트합니다.
조합의 예술 이주니어 씨가 질문했습니다. "그럼 atom들을 어떻게 연결하나요?" 김개발 씨가 웃으며 답했습니다.
"바로 조합입니다. 이게 Jotai의 세 번째 철학, 확장성의 핵심이죠." 작은 atom들을 조합해서 복잡한 로직을 만듭니다.
마치 레고 블록처럼 말이죠. 각 블록은 단순하지만, 조합하면 성을 만들 수도 있습니다.
예를 들어 장바구니 총액을 계산하는 atom을 만든다고 해봅시다: const cartTotalAtom = atom((get) => { const items = get(cartItemsAtom) return items.reduce((sum, item) => sum + item.price * item.quantity, 0) }) cartItemsAtom이 변경되면, cartTotalAtom이 자동으로 재계산됩니다. 별도의 설정이 필요 없습니다.
의존성 추적이 자동으로 이루어지니까요. 프레임워크 독립성 김개발 씨가 다음 슬라이드를 넘겼습니다.
"확장성의 또 다른 측면은 프레임워크 독립성입니다. Jotai의 핵심은 React와 무관합니다." 한 팀원이 고개를 갸우뚱했습니다.
"React용 라이브러리인데 React와 무관하다니, 무슨 말이죠?" 김개발 씨가 설명했습니다. "vanilla 폴더를 기억하시나요?
거기에 핵심 로직이 다 있고, React는 그냥 껍데기입니다. 이론적으로는 Vue나 Angular용 바인딩도 만들 수 있어요." 실제로 Jotai는 Next.js의 서버 컴포넌트에서도 사용할 수 있습니다.
React 없이도 말이죠. 이는 다른 React 상태 관리 라이브러리에서는 불가능한 일입니다.
점진적 도입 박시니어 씨가 중요한 질문을 했습니다. "기존 프로젝트에 Jotai를 도입하려면 전체를 다시 작성해야 하나요?" 김개발 씨가 자신 있게 답했습니다.
"아닙니다! 이것도 Jotai의 철학 중 하나입니다.
점진적 도입이 가능해요. Redux와 Jotai를 함께 사용할 수도 있고, 일부 컴포넌트만 Jotai로 마이그레이션할 수도 있습니다." 이는 실용주의적 접근입니다.
모든 걸 한 번에 바꾸라고 강요하지 않습니다. 작은 부분부터 시작해서, 효과를 보면 점차 확대하면 됩니다.
TypeScript 퍼스트 김개발 씨가 마지막 슬라이드를 보여줬습니다. "Jotai는 TypeScript로 작성되었고, 타입 추론이 훌륭합니다.
atom을 정의하면 타입이 자동으로 추론되어, 별도의 타입 정의가 거의 필요 없어요." 한 팀원이 감탄했습니다. "Redux에서는 액션 타입, 페이로드 타입, 스테이트 타입을 다 정의해야 했는데..." "맞아요.
Jotai는 그럴 필요가 없습니다. TypeScript의 힘을 최대한 활용하죠." 실제 사례 김개발 씨가 마무리했습니다.
"실제로 Discord, Shopify 같은 큰 회사들이 Jotai를 사용하고 있습니다. 단순함과 성능, 그리고 확장성 때문이죠." 회의가 끝나고, 팀 리드가 말했습니다.
"좋은 프레젠테이션이었어요. Jotai의 철학이 우리 팀의 '간결하고 유지보수하기 쉬운 코드'라는 가치와 잘 맞는 것 같네요." 정리하며 김개발 씨는 뿌듯했습니다.
단순히 라이브러리를 사용하는 것을 넘어, 그 철학을 이해하고 팀에 전파할 수 있었으니까요. 좋은 도구는 기능만 좋은 게 아닙니다.
명확한 철학과 가치관을 가지고 있어야 합니다. Jotai의 세 가지 철학 - 최소주의, 원자성, 확장성 - 은 단순한 슬로건이 아닙니다.
코드 구조부터 API 디자인, 내부 알고리즘까지 모든 곳에 스며들어 있습니다. 이것이 Jotai를 특별하게 만드는 이유입니다.
여러분도 다음에 라이브러리를 선택할 때, 단순히 기능 목록만 보지 마세요. 그 뒤에 숨은 설계 철학을 이해하세요.
그것이 여러분의 프로젝트와 맞는지 생각해보세요. 좋은 선택은 좋은 이해에서 시작됩니다.
실전 팁
💡 - 라이브러리를 선택할 때는 기능뿐 아니라 설계 철학도 고려하세요
- 최소주의 API는 배우기 쉽고 유지보수하기 편합니다
- 원자성 덕분에 상태를 독립적으로 관리하고 선택적 리렌더링이 가능합니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
마이크로서비스 배포 완벽 가이드
Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.
Application Load Balancer 완벽 가이드
AWS의 Application Load Balancer를 처음 배우는 개발자를 위한 실전 가이드입니다. ALB 생성부터 ECS 연동, 헬스 체크, HTTPS 설정까지 실무에 필요한 모든 내용을 다룹니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.
고객 상담 AI 시스템 완벽 구축 가이드
AWS Bedrock Agent와 Knowledge Base를 활용하여 실시간 고객 상담 AI 시스템을 구축하는 방법을 단계별로 학습합니다. RAG 기반 지식 검색부터 Guardrails 안전 장치, 프론트엔드 연동까지 실무에 바로 적용 가능한 완전한 시스템을 만들어봅니다.
에러 처리와 폴백 완벽 가이드
AWS API 호출 시 발생하는 에러를 처리하고 폴백 전략을 구현하는 방법을 다룹니다. ThrottlingException부터 서킷 브레이커 패턴까지, 실전에서 바로 활용할 수 있는 안정적인 에러 처리 기법을 배웁니다.
AWS Bedrock 인용과 출처 표시 완벽 가이드
AWS Bedrock의 Citation 기능을 활용하여 AI 응답의 신뢰도를 높이는 방법을 배웁니다. 출처 추출부터 UI 표시, 검증까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.