Skip to content

Conversation

@u-zzn
Copy link
Collaborator

@u-zzn u-zzn commented Jan 5, 2026

✏️ Summary

Comfit 서비스의 Zustand 라이브러리 초기 세팅을 진행했습니다.
전역 상태를 최소화하는 팀 컨벤션에 맞춰, 토큰은 store가 아닌 shared 유틸에서 관리하고 Zustand에는 추후에 로그인 여부(isLoggedIn)만 유지할 수 있도록 구조를 설계했습니다.

본 PR은 “초기 세팅”에 초점을 둔 작업으로, persist 방식(session/local)이나 세부 컨벤션은 추후 팀 내부 합의에 따라 변경될 수 있습니다.

📑 Tasks

✔️ Comfit은 왜 zustand를 도입했을까요?

Comfit 서비스에서는 전역 상태가 무분별하게 커지는 것을 방지하기 위해, **“전역 상태는 꼭 필요한 최소 범위만 관리한다”**는 방향으로 합의했습니다.
이에 따라 복잡한 설정이나 보일러플레이트가 필요한 상태 관리 도구 대신, 구조가 단순하고 사용 범위를 명확히 제한할 수 있는 Zustand를 전역 상태 관리 도구로 선택했습니다.

특히 로그인과 관련된 상태 중에서도, accessToken 자체를 전역 상태로 보관하는 것은 보안 및 책임 분리 측면에서 적절하지 않다고 판단했습니다. 따라서 UI 분기(로그인/비로그인 판단)에만 필요한 isLoggedIn boolean 값만 store에서 관리하고, 실제 토큰 값은 별도의 유틸 레이어에서 관리하는 구조로 결정했습니다.


✔️ Comfit의 auth 전역 상태 초기 세팅이 적용된 FSD 폴더 구조

src/
├─ app/
│  └─ store/
│     └─ index.ts
│        # app 레이어의 store 엔트리
│        # shared/model에 정의된 전역 store들을 모아 export
│        # app 레이어가 store 구현에 직접 의존하지 않도록 분리
│
├─ shared/
│  ├─ lib/
│  │  └─ auth/
│  │     └─ token.ts
│  │        # accessToken 저장/조회/삭제만 담당하는 storage 유틸
│  │        # 현재는 sessionStorage 기반
│  │        # 추후 localStorage 등으로 변경 시 이 파일만 수정
│  │
│  └─ model/
│     └─ auth/
│        ├─ auth.store.ts
│        │  # auth 전역 상태 정의 (Zustand)
│        │  # 관리 상태: isLoggedIn
│        │  # 제공 액션: login / logout / syncFromStorage
│        │  # token 자체는 보관하지 않고 tokenStorage를 통해서만 접근
│        │
│        └─ index.ts
│           # auth 모델의 공개 API(barrel export)
│           # 외부에서는 @shared/model/auth 경로로만 접근
│           # 내부 파일 구조 변경 시 외부 영향 최소화

✔️ 토큰 관리 유틸 분리 (shared/lib)

토큰은 상태가 아닌 저장 매체와의 인터페이스 역할에 가깝다고 판단하여, src/shared/lib/auth/token.ts 파일로 분리해 관리하도록 했습니다.

해당 유틸은 sessionStorage를 기반으로 하며, get / set / clear 세 가지 역할만 수행하는 단일 책임 구조를 갖습니다. 이를 통해 store나 UI 레이어가 storage 구현 세부사항에 직접 의존하지 않도록 구현 했습니다.

또한 현재는 세션 기반 로그인을 가정하고 있지만, 향후 자동 로그인 정책이나 보안 정책 변경으로 localStorage 전환이 필요해질 경우에도 이 파일만 수정하면 되도록 확장 가능성을 고려한 구조로 설계했습니다.


✔️ Auth 전역 store 구성 (shared/model)

로그인 여부는 여러 페이지와 레이아웃에서 공통으로 사용될 가능성이 높기 때문에, FSD 구조 기준으로 shared/model 레이어에 auth store를 위치시키는 것으로 팀 합의했습니다.

auth.store.ts에서는

  • 상태로 isLoggedIn을 두고
  • 액션으로 login / logout / syncFromStorage를 정의했습니다.
    이때 store는 토큰 값을 직접 소유하지 않으며, 모든 토큰 접근은 tokenStorage 유틸을 통해서만 이루어지도록 하여 역할과 책임을 명확히 분리하고자 했습니다.

또한 index.ts 파일을 통해 auth 모델의 **공개 API(barrel export)**를 구성하여, 외부에서는 항상 @shared/model/auth 경로로만 import 하도록 정리했습니다. 이를 통해 내부 파일 구조 변경 시에도 import 경로가 흔들리지 않도록 했습니다.


✔️ app 레이어 store 엔트리 구성

src/app/store/index.ts는 전역 store를 실제로 구현하는 역할이 아니라, shared/model에 정의된 store들을 모아 export 하는 엔트리 포인트로만 사용됩니다.

이 구조를 통해 app 레이어가 특정 store 구현에 직접 의존하지 않도록 하고, 추후 전역 store가 추가되더라도 import 구조나 진입 지점이 분산되지 않도록 대비하고자 하였습니다.


✔️ 앱 시작 시 로그인 상태 동기화

앱이 최초로 렌더링될 때, storage에 저장된 토큰 상태와 전역 store 상태를 일치시키기 위해 App.tsx에서 syncFromStorage()useEffect로 한 번만 호출하도록 구성했습니다.
이를 통해

  • storage에 토큰이 존재하면 isLoggedIn = true
  • 토큰이 없으면 isLoggedIn = false
    로 상태가 자동 동기화됩니다.

새로고침, 직접 URL 진입 등 다양한 진입 케이스에서도 로그인 상태가 일관되게 유지되도록 하기 위해 이와 같이 초기 세팅을 진행하였습니다.

👀 To Reviewer

  • 실제 API 연동 전 단계에서, storage → store 상태 흐름이 정상적으로 동작하는지 검증하기 위해 DevTools를 활용한 간단한 수동 테스트를 진행했습니다. syncFromStorage()가 실행되었을 때 콘솔에 남는 로그를 확인하고자 하였습니다.
  • sessionStorage에 토큰이 없는 상태에서 새로고침 → isLoggedIn = false
  • 임의의 브라우저 콘솔에서 토큰을 저장한 뒤 새로고침 → isLoggedIn = true
  • 브라우저 콘솔에서 토큰 삭제 후 다시 새로고침 → isLoggedIn = false

간단한 방식으로 테스트해보았고, 현재 구조가 의도한 대로 동작함을 확인하고 테스트를 위한 관련 콘솔은 모두 삭제 후 commit한 상태입니다. 관련 DevTools 테스트 결과 캡쳐본은 하단 Screenshot 섹션에 함께 첨부하도록 하겠습니다 !! ☺️


  • 이번 PR은 지금까지 클라 팀 내에서 합의된 방향을 기준으로, Zustand를 활용한 로그인 전역 상태의 초기 골격을 먼저 잡아본 작업입니다.
    당연히 추후 로그인 세션 구조(토큰 관리 방식), persist 전략(session vs local), store의 세부적인 폴더 구조나 컨벤션 등은 실제 기능 구현을 진행하면서 팀 논의를 통해 충분히 변경될 수 있는 부분이라고 생각하고 있습니다. 그래서 해당 PR에서는 더 나은 방향이나 우려되는 지점이 있다면 부담 없이 의견 주시면 적극 반영하고 싶습니다. ❤️‍🔥 구조·컨벤션·역할 분리 등 어떤 부분이든 편하게 코멘트 주시면 감사하겠습니다 🙇‍♀️

📸 Screenshot

참고: 콘솔에 동일 로그가 2번 찍혀서 왜그런지 찾아보았는데, 개발 환경에서 React StrictMode 등의 영향으로 동작 자체가 잘못된 것이 아니라 effect가 2회 실행되는 케이스일 수 있다고 합니다.

zu1 zu2 zu3

🔔 ETC

@github-actions
Copy link

github-actions bot commented Jan 5, 2026

🚀 빌드 결과

린트 검사 완료
빌드 성공

로그 확인하기
Actions 탭에서 자세히 보기

Copy link
Collaborator

@odukong odukong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FSD 구조에 맞춰서 storage에 접근하는 로직은 shared/lib로, storage와 로그인 여부 싱크를 맞춰주는 로직은 shared/model로 분리하신 점 구조적으로 좋은 선택같아요!
나중에 토큰 저장방식이 바뀌더라도 store 로직은 수정하지 않아도 되니 관리하기 더 수월해질 것 같습니다 수고 많으셨어요🙌🏻🩷

몇 가지 코멘트 남겨두었으니 시간 나실 때 확인해주세요 ♪(´▽`)

Comment on lines 5 to 6
type AuthState = {
isLoggedIn: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 AuthState 타입을 type으로 선언해주셨는데, 객체 타입의 확장성 측면이나 interface 사용을 지향하고 있는 Comfit 컨벤션 통일성을 위해 interface로 수정하는 건 어떨까요?

Comment on lines 15 to 29
isLoggedIn: false,
actions: {
login: (accessToken) => {
tokenStorage.set(accessToken);
set({ isLoggedIn: true });
},
logout: () => {
tokenStorage.clear();
set({ isLoggedIn: false });
},
syncFromStorage: () => {
const token = tokenStorage.get();
set({ isLoggedIn: Boolean(token) });
},
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syncFromStorage를 호출하여 storage 토큰과 전역 상태를 동기화하는 방향으로 구현해주셨는데,
useAuthStoreisLoggedInfalse로 초기화해두었기 때문에 App.tsx의 useEffect에서 syncFromStorage로 로그인 여부를 최신화(ex. false > true)하는 과정에서 false인 상태가 사용자에게 보여진 다음에 변경된 true상태가 보여지는 현상이 일어날 수도 같아요! (로그인 상태인데도 헤더의 로그인 버튼이 잠시 보인다거나 하는 식으로요!)

스토리지는 동기적으로 값을 가져오는만큼, 아래처럼 store 초기값 할당 시점에 바로 동기화시키는 방법은 어떨까요?

isLoggedIn: !!tokenStorage.get(),

Copy link
Contributor

@hummingbbird hummingbbird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

목적에 따라 lib/model 위치 구분해서 파일 생성한 점 저도 좋은 거 같습니다~ 다만 shared 레이어의 각 세그먼트 내부에서는 이미 레이어 자체가 전역 공통 영역이라는 의미를 가지기 때문에, 그 안에서 다시 auth 폴더로 한 번 더 감싸지 않아도 책임이 충분히 드러난다고 생각해요! 다른 분들 의견도 들어보고 합의하면 좋을 거 같습니다 ☺️

코리 한 번 확인해주시고 수빈이가 남긴 코리 반영하고 바로 머지해도 될 거 같아요 수고하셨어요 !! 🙇‍♀️🙇‍♀️

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 파일 이름은 자칫 '토큰 값들을 모아둔 상수 파일' 정도로 느껴질 수 있을 거 같아요! 의미가 더 드러나게 token-storage.ts로 수정해도 좋을 거 같습니다~

Copy link
Collaborator

@qowjdals23 qowjdals23 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zustand 초기 세팅하느라 수고하셨습니다 ! pr 구체적으로 작성해주셔서 이해하기 너무 편하네요 !!!

auth 쪽 공개 API를 한 번 모아두고, app 레이어에서도 store 진입점을 따로 정리해둔 덕분에 import 동선도 깔끔해진 느낌이고 ! 토큰은 storage 유틸로 따로 빼두고, store는 isLoggedIn만 들고 가면서 login / logout / syncFromStorage로 저장/삭제/동기화 흐름을 딱 정리해둔 게 이해하기도 쉽고 이후에 기능 붙이기도 편할 것 같네요 !!
너무 수고 많으셨습니다 ❤️‍🔥

@hummingbbird
Copy link
Contributor

hummingbbird commented Jan 8, 2026

잘 반영된 거 같습니다~ 어푸할게용 ~👼👼 (앗차차 충돌 해결 해주시오 !!)

Comment on lines 15 to 29
isLoggedIn: Boolean(tokenStorage.get()),
actions: {
login: (accessToken) => {
tokenStorage.set(accessToken);
set({ isLoggedIn: true });
},
logout: () => {
tokenStorage.clear();
set({ isLoggedIn: false });
},
syncFromStorage: () => {
const token = tokenStorage.get();
set({ isLoggedIn: Boolean(token) });
},
},
Copy link
Collaborator

@odukong odukong Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLoggedIn을 store 초기화 시점에 이미 tokenStorage.get()으로 최신 토큰 상태를 가져오고 있기 때문에,
토큰 상태에 대해 싱크를 맞춰주는 syncFromStorage() 액션 메서드는 중복된 기능을 수행하게 되는 것 같아 해당 메서드는 제거하는 방향은 어떨까요? :-O

코리 내용은 잘 반영된 것 확인했습니당!!

Copy link
Collaborator Author

@u-zzn u-zzn Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀 주신 것처럼 store 초기화 시점에 이미 storage와 동기화가 이루어지고 있어 syncFromStorage 액션은 중복 책임으로 판단되어 제거했습니다! 코멘트 감사합니다 🙇‍♀️

@u-zzn u-zzn requested a review from odukong January 8, 2026 17:38
@u-zzn u-zzn merged commit 704bf6d into dev Jan 8, 2026
2 checks passed
@u-zzn u-zzn deleted the init/#20/zustand-init branch January 8, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Init] Zustand 초기 세팅

5 participants