본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 10. · 14 Views
useAtom() 기본 사용법 완벽 가이드
Jotai의 핵심 훅인 useAtom()을 처음부터 끝까지 살펴봅니다. 상태 관리의 기본부터 실전 활용까지, 초급 개발자도 쉽게 따라할 수 있도록 구성했습니다.
목차
1. useAtom이란?
신입 개발자 김개발 씨가 회사에 출근한 지 한 달이 지났습니다. 오늘 아침, 팀장님이 새로운 태스크를 할당했습니다.
"useState 대신 Jotai를 써서 전역 상태를 관리해보세요." 김개발 씨는 처음 듣는 이름에 당황했습니다.
useAtom은 Jotai 라이브러리의 핵심 훅입니다. 마치 useState처럼 사용하지만, 컴포넌트 간 상태 공유가 가능합니다.
atom이라는 상태 단위를 읽고 쓰는 역할을 담당합니다. Redux보다 간단하고, Context API보다 효율적인 것이 특징입니다.
다음 코드를 살펴봅시다.
import { atom, useAtom } from 'jotai'
// atom 정의: 상태의 초기값을 설정합니다
const countAtom = atom(0)
function Counter() {
// useAtom으로 상태를 읽고 쓸 수 있습니다
const [count, setCount] = useAtom(countAtom)
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
)
}
[도입 - 첫 만남의 당황스러움] 김개발 씨는 지금까지 useState만 사용해왔습니다. 컴포넌트 안에서 상태를 관리하는 것이 전부였습니다.
그런데 팀장님이 갑자기 Jotai를 쓰라고 하니 머릿속이 하얘졌습니다. "도대체 useAtom이 뭐지?" 점심시간에 선배 개발자 박시니어 씨를 붙잡고 물었습니다.
"선배님, Jotai가 뭐예요?" 박시니어 씨는 웃으며 답했습니다. "아, 그거?
useState의 똑똑한 형이라고 생각하면 돼요." [개념 설명 - 비유로 쉽게] 그렇다면 useAtom이란 정확히 무엇일까요? 쉽게 비유하자면, useAtom은 마치 공용 사물함과 같습니다.
여러 사람이 같은 사물함을 열어볼 수 있고, 물건을 꺼내거나 넣을 수도 있습니다. useState는 개인 책상 서랍이라면, useAtom은 팀 전체가 공유하는 공간인 셈입니다.
이처럼 useAtom도 여러 컴포넌트가 같은 상태를 읽고 쓰는 역할을 담당합니다. [왜 필요한가 - 문제 상황] useAtom이 없던 시절에는 어땠을까요?
개발자들은 props drilling이라는 고통을 감수해야 했습니다. 부모 컴포넌트에서 자식, 손자, 증손자 컴포넌트까지 props를 계속 전달했습니다.
코드가 길어지고, 실수하기도 쉬웠습니다. 더 큰 문제는 중간 컴포넌트들이 불필요하게 리렌더링된다는 것이었습니다.
프로젝트가 커질수록 이런 문제는 눈덩이처럼 불어났습니다. Context API로 해결할 수도 있지만, 이것도 문제가 있었습니다.
Context가 업데이트되면 그 Context를 구독하는 모든 컴포넌트가 리렌더링됩니다. 성능 최적화가 어려웠습니다.
[해결책 - useAtom의 등장] 바로 이런 문제를 해결하기 위해 Jotai와 useAtom이 등장했습니다. useAtom을 사용하면 props drilling 없이 상태 공유가 가능해집니다.
또한 필요한 컴포넌트만 리렌더링되는 최적화도 자동으로 얻을 수 있습니다. 무엇보다 코드가 간결해진다는 큰 이점이 있습니다.
useState와 사용법이 거의 똑같아서 배우기도 쉽습니다. [코드 분석 - 단계별 설명] 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 **atom(0)**을 보면 초기값이 0인 상태를 만든다는 것을 알 수 있습니다. 이것이 핵심입니다.
atom은 컴포넌트 밖에서 정의하는 것이 일반적입니다. 다음으로 **useAtom(countAtom)**에서는 해당 atom을 읽고 쓸 수 있는 배열을 반환합니다.
useState와 똑같은 구조입니다. 마지막으로 setCount로 상태를 업데이트하면, 같은 atom을 사용하는 모든 컴포넌트에 변경사항이 전파됩니다.
[실무 활용 사례] 실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 서비스를 개발한다고 가정해봅시다.
장바구니 상태를 관리할 때 useAtom을 활용하면 헤더의 장바구니 아이콘, 상품 목록 페이지, 장바구니 페이지가 모두 같은 상태를 공유할 수 있습니다. props를 일일이 전달할 필요가 없습니다.
많은 스타트업에서 이런 패턴을 적극적으로 사용하고 있습니다. [주의사항] 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 atom을 컴포넌트 안에서 정의하는 것입니다. 이렇게 하면 컴포넌트가 렌더링될 때마다 새로운 atom이 만들어져 상태가 유지되지 않습니다.
따라서 컴포넌트 밖이나 별도 파일에서 atom을 정의해야 합니다. [정리] 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, useState의 형이라니, 이제 이해가 되네요!" useAtom을 제대로 이해하면 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - atom은 항상 컴포넌트 밖에서 정의하세요
- useState와 사용법이 거의 같으니 부담 없이 시작하세요
- 작은 프로젝트부터 적용해보면서 익숙해지세요
2. 기본 문법과 사용법
김개발 씨는 useAtom의 개념을 이해했지만, 실제로 어떻게 사용하는지 막막했습니다. "atom을 어디에 만들어야 하지?
import는 어떻게 하지?" 박시니어 씨가 다가와 말했습니다. "문법은 세 단계만 기억하면 돼요."
useAtom의 기본 문법은 세 단계로 구성됩니다. 첫째, atom을 정의합니다.
둘째, useAtom 훅을 호출합니다. 셋째, 반환된 값과 setter로 상태를 다룹니다.
useState를 써봤다면 전혀 어렵지 않습니다.
다음 코드를 살펴봅시다.
// 1단계: atom 정의 (컴포넌트 밖에서)
import { atom, useAtom } from 'jotai'
const userNameAtom = atom('김개발')
const isLoggedInAtom = atom(false)
function UserProfile() {
// 2단계: useAtom 호출
const [userName, setUserName] = useAtom(userNameAtom)
const [isLoggedIn, setIsLoggedIn] = useAtom(isLoggedInAtom)
// 3단계: 상태 읽기/쓰기
return (
<div>
<p>이름: {userName}</p>
<p>로그인: {isLoggedIn ? '예' : '아니오'}</p>
<button onClick={() => setIsLoggedIn(true)}>로그인</button>
</div>
)
}
[도입 - 막막한 시작] 김개발 씨는 코드 에디터를 열고 손가락을 키보드 위에 올렸습니다. 하지만 첫 줄부터 막혔습니다.
"import를 어떻게 하지? atom은 어디에 쓰지?" 머릿속이 복잡했습니다.
박시니어 씨가 옆에 앉으며 말했습니다. "처음엔 다들 그래요.
하지만 순서만 알면 쉬워요. useState 쓸 줄 알죠?
그거랑 거의 똑같아요." [1단계 - atom 정의하기] 첫 번째 단계는 atom을 정의하는 것입니다. atom은 마치 레고 블록의 기본 조각과 같습니다.
이 조각들을 조합해서 복잡한 구조를 만들 수 있습니다. atom을 만들 때는 초기값을 반드시 전달해야 합니다.
숫자, 문자열, 불린, 객체, 배열 무엇이든 가능합니다. 중요한 점은 atom을 컴포넌트 밖에서 정의한다는 것입니다.
그래야 컴포넌트가 리렌더링되어도 atom이 재생성되지 않습니다. 보통 파일 상단이나 별도의 atoms.ts 파일에 모아둡니다.
[2단계 - useAtom 호출하기] 두 번째 단계는 useAtom 훅을 호출하는 것입니다. 컴포넌트 안에서 useAtom(countAtom)처럼 호출하면 됩니다.
그러면 배열을 반환합니다. 첫 번째 요소는 현재 값, 두 번째 요소는 setter 함수입니다.
useState와 정확히 같은 구조입니다. 구조 분해 할당으로 받는 것이 일반적입니다.
const [value, setValue] = useAtom(myAtom)처럼 말이죠. 이렇게 하면 코드가 깔끔해집니다.
[3단계 - 상태 사용하기] 세 번째 단계는 받은 값으로 상태를 읽고 쓰는 것입니다. 첫 번째 요소인 value는 JSX에서 그대로 사용할 수 있습니다.
{userName}처럼 중괄호 안에 넣으면 화면에 표시됩니다. 두 번째 요소인 setValue는 이벤트 핸들러에서 호출합니다.
setUserName('박시니어')처럼 새 값을 전달하면 상태가 업데이트됩니다. [코드 분석 - 실제 예제] 위의 코드를 다시 살펴보겠습니다.
userNameAtom과 isLoggedInAtom 두 개의 atom을 정의했습니다. 하나는 문자열, 하나는 불린 값입니다.
컴포넌트 안에서 각각 useAtom으로 꺼내 사용합니다. 버튼을 클릭하면 setIsLoggedIn(true)가 호출되어 로그인 상태가 바뀝니다.
같은 atom을 다른 컴포넌트에서도 사용할 수 있습니다. 예를 들어 Header 컴포넌트에서도 useAtom(userNameAtom)을 호출하면 같은 이름이 표시됩니다.
한쪽에서 이름을 바꾸면 다른 쪽도 자동으로 업데이트됩니다. [타입스크립트와 함께] 타입스크립트를 사용한다면 더욱 안전합니다.
atom의 타입은 초기값으로부터 자동 추론됩니다. atom(0)이면 number 타입, atom('')이면 string 타입입니다.
명시적으로 타입을 지정하고 싶다면 atom<number>(0)처럼 제네릭을 사용할 수 있습니다. useAtom의 반환 타입도 자동으로 추론됩니다.
별도로 타입을 작성할 필요가 없어 편리합니다. [실무 팁] 실무에서는 atom을 한곳에 모아두는 것이 좋습니다.
예를 들어 src/atoms/user.ts 파일을 만들고, 사용자 관련 atom들을 모두 export합니다. 그러면 import { userNameAtom, isLoggedInAtom } from '@/atoms/user'처럼 깔끔하게 가져올 수 있습니다.
프로젝트가 커져도 관리하기 쉽습니다. [정리] 김개발 씨는 박시니어 씨의 설명을 들으며 직접 코드를 작성해봤습니다.
"오, 진짜 useState랑 똑같네요!" 세 단계 문법을 익히니 자신감이 생겼습니다. 이제 여러분도 useAtom의 기본 문법을 마스터했습니다.
다음 단계로 넘어가 봅시다.
실전 팁
💡 - atom은 atoms 폴더에 도메인별로 정리하세요
- useState 사용법을 그대로 적용하면 됩니다
- 타입스크립트를 쓴다면 타입 추론을 신뢰하세요
3. 간단한 예제 코드
박시니어 씨가 김개발 씨에게 실습 과제를 냈습니다. "카운터 앱을 두 개의 컴포넌트로 나눠서 만들어보세요.
한쪽에서 숫자를 올리면 다른 쪽에도 반영되어야 합니다." 김개발 씨는 useAtom을 떠올리며 코딩을 시작했습니다.
카운터 앱은 useAtom 학습의 대표적인 예제입니다. 하나의 atom을 여러 컴포넌트에서 공유하는 방법을 보여줍니다.
버튼을 클릭하면 숫자가 증가하고, 모든 컴포넌트에 즉시 반영됩니다. Props drilling 없이 깔끔하게 구현할 수 있습니다.
다음 코드를 살펴봅시다.
import { atom, useAtom } from 'jotai'
// 카운트 상태를 담는 atom
const countAtom = atom(0)
// 카운터를 표시하는 컴포넌트
function CounterDisplay() {
const [count] = useAtom(countAtom)
return <h1>현재 카운트: {count}</h1>
}
// 카운터를 조작하는 컴포넌트
function CounterButtons() {
const [count, setCount] = useAtom(countAtom)
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>리셋</button>
</div>
)
}
// 메인 앱
function App() {
return (
<div>
<CounterDisplay />
<CounterButtons />
</div>
)
}
[도입 - 과제 시작] 김개발 씨는 에디터를 열고 빈 파일을 응시했습니다. "두 개의 컴포넌트를 어떻게 연결하지?
props를 쓰면 되나?" 하지만 과제 조건은 props를 사용하지 말라는 것이었습니다. 그때 머릿속에 박시니어 씨의 말이 떠올랐습니다.
"공용 사물함이라고 생각하면 돼요." 김개발 씨는 useAtom을 떠올리며 코딩을 시작했습니다. [atom 정의하기] 가장 먼저 할 일은 카운트를 저장할 atom을 만드는 것입니다.
countAtom이라는 이름으로 초기값 0을 가진 atom을 만들었습니다. 이 atom이 두 컴포넌트를 연결하는 다리 역할을 합니다.
마치 두 건물을 연결하는 육교처럼, atom은 컴포넌트들을 이어줍니다. [표시 컴포넌트 만들기] 다음은 숫자를 화면에 보여주는 컴포넌트를 만들 차례입니다.
CounterDisplay 컴포넌트는 오직 읽기만 합니다. useAtom(countAtom)을 호출하되, setter는 사용하지 않습니다.
const [count] = useAtom(countAtom)처럼 첫 번째 값만 받으면 됩니다. 이렇게 하면 현재 카운트를 화면에 표시할 수 있습니다.
[조작 컴포넌트 만들기] 이제 숫자를 바꾸는 컴포넌트를 만들어봅시다. CounterButtons 컴포넌트는 읽기와 쓰기를 모두 합니다.
useAtom으로 count와 setCount를 모두 받습니다. 세 개의 버튼을 만들어 각각 증가, 감소, 리셋 기능을 구현했습니다.
setCount를 호출하면 atom이 업데이트됩니다. [마법 같은 동기화] 여기서 놀라운 일이 벌어집니다.
CounterButtons에서 setCount를 호출하면 즉시 CounterDisplay의 화면이 바뀝니다. 두 컴포넌트는 서로를 전혀 모릅니다.
props로 연결되어 있지도 않습니다. 하지만 같은 atom을 바라보고 있기 때문에 자동으로 동기화됩니다.
이것이 바로 useAtom의 핵심입니다. 컴포넌트 간의 의존성 없이 상태를 공유할 수 있습니다.
[코드 흐름 따라가기] 사용자가 +1 버튼을 클릭하는 순간을 따라가 봅시다. onClick 핸들러가 실행되고 setCount(count + 1)이 호출됩니다.
countAtom의 값이 0에서 1로 바뀝니다. Jotai는 이 atom을 구독하는 모든 컴포넌트를 찾습니다.
CounterDisplay와 CounterButtons가 리렌더링됩니다. 화면에 "현재 카운트: 1"이 표시됩니다.
모든 과정이 순식간에 일어납니다. [실무 확장 가능성] 이 패턴은 실무에서 무한히 확장됩니다.
장바구니 앱을 만든다고 가정해봅시다. cartAtom을 만들고, 상품 목록 페이지에서 상품을 추가하면 헤더의 장바구니 아이콘 숫자가 바뀝니다.
장바구니 페이지에서 상품을 삭제하면 다시 헤더가 업데이트됩니다. 복잡한 props drilling 없이 말이죠.
[성능 이야기] "그럼 모든 컴포넌트가 리렌더링되는 거 아니에요?" 김개발 씨가 물었습니다. 박시니어 씨가 답했습니다.
"아니에요. Jotai는 똑똑해요.
countAtom을 사용하는 컴포넌트만 리렌더링됩니다. 다른 atom을 쓰는 컴포넌트는 영향받지 않아요." 이것이 Context API보다 Jotai가 우수한 이유입니다.
세밀한 최적화가 자동으로 이루어집니다. [정리] 김개발 씨는 과제를 완성했습니다.
두 개의 컴포넌트가 props 없이 완벽하게 동기화되는 것을 보며 신기해했습니다. "이게 되네요!" 이제 여러분도 간단한 예제를 직접 만들 수 있습니다.
카운터를 만들어보고, 다른 기능으로 확장해보세요.
실전 팁
💡 - 읽기만 필요하면 setter를 생략할 수 있습니다
- 여러 컴포넌트에서 같은 atom을 자유롭게 사용하세요
- 작은 예제부터 시작해서 점진적으로 확장하세요
4. 실제 소스 코드 분석
김개발 씨가 과제를 제출하자 박시니어 씨가 코드 리뷰를 시작했습니다. "좋아요.
그런데 실무 코드는 좀 더 구조화되어 있어야 해요." 박시니어 씨는 실제 프로젝트 코드를 열어 보여줬습니다. "이렇게 작성하면 유지보수가 훨씬 쉬워요."
실무에서는 atom을 별도 파일로 분리하고, 타입을 명시하며, 의미 있는 이름을 사용합니다. 컴포넌트는 역할별로 나누고, 각각 필요한 atom만 구독합니다.
폴더 구조도 체계적으로 관리하여 팀원들이 쉽게 이해할 수 있도록 합니다.
다음 코드를 살펴봅시다.
// src/atoms/todo.ts - atom 정의
import { atom } from 'jotai'
export interface Todo {
id: number
text: string
completed: boolean
}
export const todoListAtom = atom<Todo[]>([])
export const todoCountAtom = atom((get) => get(todoListAtom).length)
// src/components/TodoInput.tsx - 입력 컴포넌트
import { useAtom } from 'jotai'
import { todoListAtom } from '@/atoms/todo'
export function TodoInput() {
const [todos, setTodos] = useAtom(todoListAtom)
const [input, setInput] = useState('')
const addTodo = () => {
if (!input.trim()) return
setTodos([...todos, { id: Date.now(), text: input, completed: false }])
setInput('')
}
return (
<div>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={addTodo}>추가</button>
</div>
)
}
// src/components/TodoCount.tsx - 카운트 표시
import { useAtom } from 'jotai'
import { todoCountAtom } from '@/atoms/todo'
export function TodoCount() {
const [count] = useAtom(todoCountAtom)
return <p>총 {count}개의 할 일</p>
}
[도입 - 프로 코드와의 만남] 김개발 씨는 박시니어 씨의 코드를 보고 놀랐습니다. 자신이 작성한 코드보다 훨씬 체계적이었습니다.
"이렇게까지 해야 하나요?" 박시니어 씨가 답했습니다. "작은 프로젝트는 상관없지만, 실무에서는 이렇게 해야 협업이 편해요." [파일 구조 - 관심사의 분리] 첫 번째 특징은 파일을 역할별로 나눈다는 것입니다.
atoms 폴더에는 상태 정의만 들어갑니다. components 폴더에는 UI 컴포넌트만 들어갑니다.
이렇게 하면 나중에 atom을 찾을 때 한곳만 보면 됩니다. 프로젝트가 커져도 혼란스럽지 않습니다.
마치 도서관에서 책을 분류하는 것과 같습니다. 소설은 소설 구역에, 과학책은 과학 구역에 놓으면 찾기 쉽습니다.
코드도 마찬가지입니다. [타입 정의 - 안전한 코드] 두 번째 특징은 인터페이스로 타입을 명시한다는 것입니다.
Todo 인터페이스를 정의하여 할 일의 구조를 명확히 했습니다. id는 숫자, text는 문자열, completed는 불린입니다.
이렇게 하면 실수로 잘못된 타입을 넣는 것을 방지할 수 있습니다. 타입스크립트가 컴파일 시점에 에러를 잡아줍니다.
atom에 제네릭을 사용한 것도 주목할 점입니다. atom<Todo[]>([])는 "이 atom은 Todo 배열을 저장한다"는 의미입니다.
타입 안정성이 보장됩니다. [파생 atom - 계산된 값] 세 번째 특징은 todoCountAtom처럼 파생 atom을 사용한다는 것입니다.
파생 atom은 다른 atom의 값을 기반으로 계산됩니다. todoListAtom이 바뀌면 todoCountAtom도 자동으로 업데이트됩니다.
직접 카운트를 세는 코드를 여러 곳에 작성할 필요가 없습니다. atom((get) => get(todoListAtom).length)는 "todoListAtom을 읽어서 길이를 반환하라"는 의미입니다.
get 함수로 다른 atom을 읽을 수 있습니다. 이것이 Jotai의 강력한 기능 중 하나입니다.
[컴포넌트 분리 - 단일 책임] 네 번째 특징은 컴포넌트를 작은 단위로 나눈다는 것입니다. TodoInput은 오직 할 일을 추가하는 역할만 합니다.
TodoCount는 개수를 표시하는 역할만 합니다. 각 컴포넌트가 하나의 책임만 가지므로 테스트하기 쉽고 재사용하기 좋습니다.
useState와 useAtom을 함께 사용한 것도 주목하세요. input은 로컬 상태로 관리하고, todos는 전역 상태로 관리합니다.
모든 것을 atom으로 만들 필요는 없습니다. 적재적소에 사용하는 것이 중요합니다.
[import 경로 - 절대 경로 사용] 다섯 번째 특징은 @를 사용한 절대 경로입니다. '@/atoms/todo'처럼 @로 시작하면 프로젝트 루트부터의 경로를 의미합니다.
상대 경로인 '../../../atoms/todo'보다 훨씬 깔끔합니다. 파일 위치가 바뀌어도 import 경로를 수정할 필요가 없습니다.
이것은 tsconfig.json이나 vite.config.ts에서 설정할 수 있습니다. 실무 프로젝트에서는 거의 필수입니다.
[에러 처리 - 방어적 코딩] 여섯 번째 특징은 입력값 검증입니다. addTodo 함수를 보면 if (!input.trim()) return이 있습니다.
빈 문자열이나 공백만 입력하면 아무 일도 일어나지 않습니다. 이런 방어 코드가 버그를 예방합니다.
실무에서는 이런 작은 검증들이 쌓여서 안정적인 서비스를 만듭니다. [정리] 김개발 씨는 박시니어 씨의 코드를 꼼꼼히 읽어봤습니다.
"확실히 제 코드보다 체계적이네요. 이렇게 작성하면 나중에 수정하기도 쉽겠어요." 실무 코드는 지금 당장만 보는 것이 아닙니다.
6개월 후, 1년 후에도 이해할 수 있어야 합니다. 여러분도 이런 구조를 익혀두면 큰 도움이 될 것입니다.
실전 팁
💡 - atoms 폴더를 만들어 상태를 한곳에 모으세요
- 파생 atom을 활용하면 중복 코드를 줄일 수 있습니다
- 타입스크립트를 사용하면 실수를 크게 줄일 수 있습니다
5. 자주 하는 실수와 해결법
김개발 씨는 실제 프로젝트에 useAtom을 적용하다가 이상한 버그를 만났습니다. 버튼을 클릭해도 화면이 업데이트되지 않았습니다.
"분명히 setCount를 호출했는데 왜 안 바뀌지?" 새벽 2시, 김개발 씨는 모니터를 응시하며 한숨을 쉬었습니다.
useAtom을 처음 사용할 때는 몇 가지 흔한 실수를 하기 쉽습니다. 컴포넌트 안에서 atom을 정의하거나, 객체를 직접 수정하거나, 초기값을 잘못 설정하는 등의 실수가 대표적입니다.
이런 실수들을 알고 있으면 디버깅 시간을 크게 줄일 수 있습니다.
다음 코드를 살펴봅시다.
// 실수 1: 컴포넌트 안에서 atom 정의
function BadCounter() {
const countAtom = atom(0) // 매번 새 atom이 생성됨!
const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
// 올바른 방법: 컴포넌트 밖에서 정의
const countAtom = atom(0)
function GoodCounter() {
const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
// 실수 2: 객체/배열 직접 수정
function BadTodoList() {
const [todos, setTodos] = useAtom(todosAtom)
const addTodo = (text: string) => {
todos.push({ id: Date.now(), text }) // 직접 수정하면 리렌더링 안 됨!
setTodos(todos)
}
}
// 올바른 방법: 새 배열 생성
function GoodTodoList() {
const [todos, setTodos] = useAtom(todosAtom)
const addTodo = (text: string) => {
setTodos([...todos, { id: Date.now(), text }]) // 새 배열 생성
}
}
[도입 - 버그와의 사투] 김개발 씨는 코드를 다시 읽어봤습니다. 문법은 맞는 것 같았습니다.
useAtom도 제대로 호출했습니다. "도대체 뭐가 문제지?" 옆자리의 박시니어 씨도 퇴근한 지 오래였습니다.
다음날 아침, 박시니어 씨가 출근하자마자 김개발 씨의 코드를 봤습니다. "아, 여기 문제네요.
atom을 컴포넌트 안에서 만들었어요." [실수 1 - 컴포넌트 안 atom 정의] 가장 흔한 실수는 atom을 컴포넌트 안에서 정의하는 것입니다. 컴포넌트가 리렌더링될 때마다 새로운 atom이 만들어집니다.
이전 상태는 사라지고 항상 초기값으로 돌아갑니다. 버튼을 아무리 눌러도 카운트가 올라가지 않는 것처럼 보입니다.
실제로는 매번 새 atom에 +1을 하고 있는 것입니다. 해결법은 간단합니다.
atom을 컴포넌트 밖으로 빼내세요. 파일 상단이나 별도의 atoms.ts 파일에 정의하면 됩니다. 그러면 컴포넌트가 리렌더링되어도 같은 atom을 계속 사용합니다.
[실수 2 - 직접 수정] 두 번째 흔한 실수는 객체나 배열을 직접 수정하는 것입니다. todos.push()처럼 배열에 직접 push를 하면 배열 자체는 같은 참조를 유지합니다.
React는 참조가 같으면 변경사항이 없다고 판단합니다. 따라서 리렌더링이 일어나지 않습니다.
상태는 바뀌었는데 화면은 그대로인 이상한 상황이 발생합니다. 해결법은 불변성을 지키는 것입니다.
스프레드 연산자로 새 배열을 만드세요. [...todos, newTodo]처럼 말이죠.
객체도 마찬가지입니다. { ...user, name: 'new name' }처럼 새 객체를 만들어야 합니다.
[실수 3 - 잘못된 초기값] 세 번째 실수는 초기값을 잘못 설정하는 것입니다. atom(null)이나 atom(undefined)로 만들고, 나중에 객체를 넣으려고 하면 타입 에러가 발생할 수 있습니다.
타입스크립트에서 특히 문제가 됩니다. 해결법은 제네릭으로 타입을 명시하는 것입니다.
atom<User | null>(null)처럼 하면 null과 User 둘 다 허용됩니다. 또는 초기값을 빈 객체나 빈 배열로 설정하는 것도 방법입니다.
[실수 4 - 비동기 처리 오해] 네 번째 실수는 setState가 즉시 반영된다고 착각하는 것입니다. setCount(count + 1) 다음 줄에서 바로 console.log(count)를 하면 여전히 이전 값이 출력됩니다.
setState는 비동기로 동작하기 때문입니다. 이것은 useAtom만의 문제가 아니라 useState도 마찬가지입니다.
해결법은 함수형 업데이트를 사용하는 것입니다. setCount(prev => prev + 1)처럼 하면 이전 값을 정확히 받아올 수 있습니다.
연속으로 여러 번 업데이트해도 안전합니다. [실수 5 - 너무 많은 atom] 다섯 번째 실수는 모든 것을 atom으로 만드는 것입니다.
입력 필드의 임시 값까지 atom으로 만들 필요는 없습니다. 컴포넌트 로컬에서만 사용하는 상태는 useState로 충분합니다.
atom은 여러 컴포넌트가 공유해야 할 때만 사용하세요. 너무 많은 atom을 만들면 오히려 관리가 어려워집니다.
어떤 atom이 어디서 쓰이는지 추적하기 힘들어집니다. [디버깅 팁] 버그를 찾을 때는 이렇게 해보세요.
첫째, atom이 컴포넌트 밖에 있는지 확인하세요. 둘째, setState에서 새 객체/배열을 만드는지 확인하세요.
셋째, React Developer Tools로 atom 값을 확인해보세요. 넷째, console.log로 렌더링 횟수를 체크해보세요.
대부분의 버그는 이 네 가지 중 하나입니다. [정리] 김개발 씨는 박시니어 씨의 지적대로 atom을 컴포넌트 밖으로 빼냈습니다.
"아, 이제 되네요!" 한 줄을 수정했을 뿐인데 모든 것이 정상 작동했습니다. 실수는 누구나 합니다.
중요한 것은 같은 실수를 반복하지 않는 것입니다. 여러분도 이 다섯 가지 실수를 기억해두세요.
실전 팁
💡 - atom은 항상 컴포넌트 밖에 정의하세요
- 객체/배열은 스프레드 연산자로 복사하세요
- 로컬 상태는 useState를 사용하세요
6. 실전 활용 팁
김개발 씨는 이제 useAtom을 능숙하게 사용할 수 있게 되었습니다. 하지만 박시니어 씨는 말했습니다.
"기본은 마스터했네요. 이제 고급 테크닉을 알려줄게요." 김개발 씨의 눈이 반짝였습니다.
useAtom을 실전에서 활용하려면 몇 가지 고급 패턴을 알아야 합니다. 읽기 전용 atom, 쓰기 전용 atom, localStorage 연동, 비동기 atom 등의 테크닉이 대표적입니다.
이런 패턴을 익히면 더 강력하고 유연한 상태 관리가 가능합니다.
다음 코드를 살펴봅시다.
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
// 읽기 전용 hook 사용
function ReadOnlyComponent() {
const count = useAtomValue(countAtom) // setter 불필요
return <p>{count}</p>
}
// 쓰기 전용 hook 사용
function WriteOnlyComponent() {
const setCount = useSetAtom(countAtom) // value 불필요
return <button onClick={() => setCount(10)}>10으로 설정</button>
}
// localStorage와 연동
const persistedAtom = atom(
localStorage.getItem('key') || 'default',
(get, set, newValue: string) => {
set(persistedAtom, newValue)
localStorage.setItem('key', newValue)
}
)
// 비동기 atom
const userAtom = atom(async () => {
const response = await fetch('/api/user')
return response.json()
})
[도입 - 다음 단계로] 김개발 씨는 자신감이 붙었습니다. useAtom으로 간단한 상태 관리는 문제없었습니다.
하지만 프로젝트가 복잡해지면서 새로운 요구사항이 생겼습니다. "로그인 상태를 브라우저에 저장하고 싶어요.
새로고침해도 유지되게요." 박시니어 씨가 미소 지으며 말했습니다. "좋은 질문이네요.
이제 진짜 실전 테크닉을 배울 때가 됐어요." [패턴 1 - 읽기 전용 hook] 첫 번째 테크닉은 useAtomValue를 사용하는 것입니다. 어떤 컴포넌트는 상태를 읽기만 하고 변경하지 않습니다.
이럴 때 useAtom 대신 useAtomValue를 쓰면 코드가 더 명확해집니다. const count = useAtomValue(countAtom)처럼 사용합니다.
setter가 없으니 실수로 상태를 바꿀 일도 없습니다. 이것은 코드를 읽는 사람에게 "이 컴포넌트는 상태를 변경하지 않아요"라고 알려주는 신호입니다.
의도가 명확해집니다. [패턴 2 - 쓰기 전용 hook] 두 번째 테크닉은 useSetAtom을 사용하는 것입니다.
반대로 어떤 컴포넌트는 상태를 변경하기만 하고 읽지 않습니다. 리셋 버튼 같은 경우입니다.
useSetAtom을 쓰면 불필요한 리렌더링을 막을 수 있습니다. countAtom이 바뀌어도 이 컴포넌트는 리렌더링되지 않습니다.
마치 쓰기 전용 USB처럼, 보내기만 하고 받지는 않는 것입니다. 성능 최적화에 도움이 됩니다.
[패턴 3 - localStorage 연동] 세 번째 테크닉은 localStorage와 연동하는 것입니다. atom의 두 번째 인자로 setter 함수를 전달하면 상태가 바뀔 때마다 추가 로직을 실행할 수 있습니다.
여기서 localStorage.setItem을 호출하면 브라우저에 자동 저장됩니다. 페이지를 새로고침해도 상태가 유지됩니다.
로그인 상태, 테마 설정, 사용자 선호도 같은 것들을 이렇게 관리하면 편리합니다. 서버 없이도 상태를 영속화할 수 있습니다.
[패턴 4 - 비동기 atom] 네 번째 테크닉은 비동기 데이터를 다루는 것입니다. atom의 초기값으로 async 함수를 전달하면 Jotai가 알아서 로딩 상태를 관리합니다.
Suspense와 함께 사용하면 더욱 강력합니다. API 호출, 데이터베이스 쿼리 같은 비동기 작업을 깔끔하게 처리할 수 있습니다.
const user = useAtomValue(userAtom)처럼 사용하면 자동으로 데이터를 가져옵니다. 로딩 중이면 Suspense fallback이 표시됩니다.
에러가 나면 ErrorBoundary가 잡아줍니다. [패턴 5 - atom 조합] 다섯 번째 테크닉은 여러 atom을 조합하는 것입니다.
atom((get) => {...})처럼 파생 atom을 만들 수 있습니다. 여러 atom의 값을 합치거나, 필터링하거나, 계산할 수 있습니다.
예를 들어 완료되지 않은 할 일만 보여주는 atom을 만들 수 있습니다. const incompleteTodosAtom = atom((get) => { const todos = get(todosAtom) return todos.filter(todo => !todo.completed) }) 이렇게 하면 비즈니스 로직을 atom에 캡슐화할 수 있습니다.
컴포넌트는 단순히 표시만 하면 됩니다. [패턴 6 - DevTools 활용] 여섯 번째 테크닉은 Jotai DevTools를 사용하는 것입니다.
jotai-devtools 패키지를 설치하면 모든 atom의 상태를 시각적으로 볼 수 있습니다. 어떤 atom이 언제 바뀌는지, 어떤 컴포넌트가 구독하는지 한눈에 파악할 수 있습니다.
디버깅이 훨씬 쉬워집니다. Redux DevTools처럼 시간 여행 디버깅도 가능합니다.
과거 상태로 돌아가서 버그를 재현할 수 있습니다. [실무 조합 패턴] 이런 테크닉들을 조합하면 강력합니다.
예를 들어 사용자 인증 시스템을 만든다면, 비동기 atom으로 사용자 정보를 가져오고, localStorage로 토큰을 저장하고, 파생 atom으로 로그인 여부를 계산할 수 있습니다. 각 컴포넌트는 useAtomValue나 useSetAtom으로 필요한 부분만 사용합니다.
복잡한 로직이지만 코드는 깔끔하게 유지됩니다. [정리] 김개발 씨는 노트에 열심히 메모했습니다.
"이런 패턴들이 있었군요. 프로젝트에 바로 적용해봐야겠어요." 박시니어 씨가 어깨를 두드렸습니다.
"이제 useAtom 마스터예요!" 고급 패턴을 익히면 더 우아하고 효율적인 코드를 작성할 수 있습니다. 여러분도 하나씩 시도해보세요.
실전 팁
💡 - 읽기만 하면 useAtomValue, 쓰기만 하면 useSetAtom을 쓰세요
- localStorage 연동으로 상태를 영속화할 수 있습니다
- 비동기 atom과 Suspense를 함께 사용하면 강력합니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (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 표시, 검증까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.