단계별 상태 관리 라이브러리 @use-funnel을 어떻게 만들게 되었는지 소개드릴게요. 문제의식을 공유하고 해결방법을 찾으려는 분들께 도움이 되면 좋겠어요.
기술 블로그 모음
국내 IT 기업들의 기술 블로그 글을 한 곳에서 모아보세요


안녕하세요. 올리브영에서 UI…

…
토스 채용 꿀팁, 면접관들이 직접 말합니다! 토스의 문동욱 님과 이성준 님이 채용 과정에 대한 솔직한 이야기부터 실질적인 꿀팁까지, 지원한다면 꼭 알아야 할 정보를 모두 모았습니다. 절대 놓치지 마세요!

안녕하세요. LINE Plus ABC Studio에서 앱 개발을 하고 있는 김종식, 최정연, 박유진입니다. 저희 팀은 Flutter를 활용해 일본에서 운영하는 배달 서비스인 '데마...

목차 시작하며 문제점들 2.1 환경변수 일원화 2.2 환경 변수를 만들기까지의 과정 쉘 스크립트를 만들어보자 CLI를 만들어보자 마무리 1. 시작하며 안녕하세요 common resource 팀의 카인입니다. common resource 팀은 개발자들의 일상적인 작업을 돕는 여러 도구들을 지속적으로 개발하고 있습니다. 우리는 프로젝트를 진행하면서 환경 ...

안녕하세요. 질문을 사랑하는 올리브영 프론트엔드 개발자 “우문Hyun답”입니다.😁 스토어전시 스쿼드에서는 24년 2-3분기에 기존 JSP로 구현된 기획전 시스템을 Next.js…

상태 관리 라이브러리 vanilla-store를 소개합니다!안녕하세요. 내자산&회원FE팀 김현석입니다. 리액트를 사용하는 개발자들에게 상태 관리는 항상 중요한 고민거리입니다. 갈수록 커지고 복잡해지는 앱은 상태 관리를 더 어렵게 만들고 있으며 이를 효과적으로 다루기 위해 상태 관리 라이브러리를 사용해 개발하는 경우가 많습니다. Redux, MobX, Recoil, Jotai 등 많은 상태 관리 라이브러리들이 있고 네이버페이에서도 각 팀의 필요에 맞는 라이브러리를 선택해 개발하고 있습니다.제가 속한 팀에서는 mobx를 주로 상태 관리라이브러리로 사용했습니다. mobx는 다른 라이브러리에 비해 보일러 플레이트 코드가 적고, observer로 컴포넌트를 감싸주기만 하면 상태가 변했을 때 알아서 업데이트를 수행합니다. 이러한 간결함 덕분에 비즈니스 로직에 집중할 수 있는 장점이 있었고 많은 서비스들을 mobx를 사용해 개발했었습니다.그러던 중 담당 업권 서비스가 복잡해지면서 별도 레포로 이관하게 되었는데 이 과정에서 불필요한 의존성을 줄이자는 기술적인 목표를 설정했습니다. 상태 관리 라이브러리도 그 대상으로 최대한 react-api를 활용하면 이를 대체할 수 있지 않을까라는 생각으로 작업을 진행했습니다.생각보다 많은 부분들을 react-api를 통해 대체할 수 있었습니다. 하지만 작업을 진행하면서 mobx의 부재가 느껴지는 부분이 존재했고 결국 상태 관리 라이브러리의 기능이 필요하다는 결론을 내렸습니다. 다만 mobx를 다시 도입할 정도로 큰 문제는 아니었기에 지금 우리에게 필요한 기능만을 가진 간단한 상태 관리 라이브러리를 만들어 적용하면 어떨까라는 생각으로 vanilla-store를 만들게 되었습니다. 아래 글을 통해서 vanilla-store의 내부 구현, 사용 예제, 실제 사용 예시를 소개합니다.useSyncExternalStore본격적으로 vanilla-store에 대해서 소개하기에 앞서서 리액트의 상태 관리에 대해서 살펴보겠습니다.리액트 생태계 내부에서의 상태 관리는 우리가 익히 알고 있는 `useState, this.setState`등을 통해 이루어집니다. 상태 값이 바뀌면 해당 useState를 포함한 컴포넌트가 리렌더링되어 변경된 상태를 형상에 반영합니다.그럼 리액트 생태계 외부에서는 어떻게 상태 관리를 하면 될까요? 아래 코드처럼 외부에 상태 관리를 해도 될까요?let count = 0function Component() { function handleClick() { count += 1 } return <div onClick={handleClick}>{count}</div>}짐작하셨겠지만 전역변수 count가 바뀌어도 형상에는 반영되지 않습니다. 리액트는 아래 경우에서만 리렌더링을 트리거 합니다.- 클래스 컴포넌트에서 `this.setState`,`forceUpdate` - 함수 컴포넌트에서 `useState`, `useReducer` - 컴포넌트의 props가 변경되는 경우- 부모 컴포넌트가 리렌더링 된 경우리액트 생태계 외부의 상태 값이 바뀌어도 리액트 업데이트 큐에 반영되지 않기에 형상이 바뀌지 않는 것이죠. 외부의 상태가 바뀌었을 때 리액트에 이를 알려주고 생명주기에 맞게 업데이트해주는 무언가가 필요합니다. 리액트는 이를 지원하기 위해 useSyncExternalStore 훅을 제공합니다.useSyncExternalStore는 react18에서 새롭게 도입된 훅으로 외부 스토어와의 동기화를 간편하게 처리할 수 있게 해줍니다.useSyncExternalStore( subscribe: (callback) => Unsubscribe getSnapshot: () => State getServerSnapshot?: () => State) => StateuseSyncExternalStore는 세 매개변수를 받습니다.subscribe는 callback을 받아 등록하고 cleanup으로 등록한 callback을 제거하는 함수입니다. 말이 좀 복잡한데 매개변수로 넘어오는 callback이 리액트의 리렌더링을 트리거 하는 요소로 외부 스토어에서 이를 저장했다가 상태가 변경될 때 callback을 실행해 리액트와 싱크를 맞출 수 있게 합니다.getSnapshot은 렌더링 된 이후 값이 변경되었는지, 문자나 숫자처럼 immutable 한 값인지 확인하는데 사용됩니다. useSyncExternalStore은 이 확인이 끝난 immutable 한 값을 반환합니다.옵셔널 하게 받는 getServerSnapshot은 이름처럼 getSnapshot과 같지만 서버 렌더링의 안정성을 보장하기 위해 사용합니다.useSyncExternalStore는 또한 react18에서 concurrent 렌더링 사용 시 발생할 수 있는 ui 불일치 문제 tearing 을 해결해 줍니다.https://github.com/reactwg/react-18/discussions/69#discussion-3450021 / react disscussionreact18 이전까지는 위 그림처럼 동기적 렌더링만 지원되었습니다. 따라서 바뀐 상태에 맞게 일관된 형상을 노출할 수 있었습니다.https://github.com/reactwg/react-18/discussions/69#discussion-3450021 / react disscussion이와 다르게 concurrent 렌더링은 다른 우선순위가 높은 작업을 먼저 수행하기 위해 현재 렌더링 작업을 일시 중단시킬 수 있습니다.위 그림에서 모든 노드가 파란색이 되기 전 렌더링이 중단되고 그사이 바뀐 빨간색으로 모든 노드의 색이 불일치하는 걸 볼 수 있습니다. 렌더링이 비동기적으로 이루어지고 외부 스토어의 값이 그 사이에 변경되었을 때 문제가 발생하게 되는 것입니다.useSyncExternalStore는 React 18의 concurrent 렌더링 환경에서도 외부 스토어와의 동기화를 보장합니다. 이는 React 18 이전의 동기적(blocking) 렌더링과 유사한 일관성을 제공하여 tearing 문제를 해결합니다.내부적으로 변경사항을 dom에 적용하기 직전에 getSnapshot을 한 번 더 호출하여 처음 호출했을 때와 다른 값을 반환하면 (tearing 문제가 발생했다면) 업데이트를 다시 처음부터 시작해 일관된 형상을 노출할 수 있게 합니다.useSyncExternalStore는 react 18의 concurrent 렌더링을 상정하고 만들어졌기에 그 이전 버전에서는 사용할 수 없습니다. 문제는 react 18로의 전환은 breaking change가 많기 때문에 라이브러리 개발자의 부담이 크다는 것입니다. 이를 해결하기 위해 리액트 팀에서는 react 17 이하에서도 사용할 수 있는 useSyncExternalStore shim 패키지를 제공해 점진적인 마이그레이션을 지원합니다.상기한 이점들로 인해 recoil, zustand 등 많은 상태 관리 라이브러리들에서도 내부에 useSyncExternalStore를 사용하는 것을 볼 수 있습니다. 그리고 이제 소개할 vanilla-store도 useSyncExternalStore를 사용해 구현되었습니다!vanilla-store만들게 된 배경mobx를 제거하고 react-api useContext, useReducer 등을 사용해 상당 부분 상태 관리 라이브러리를 대체할 수 있었지만 동시에 문제점도 나타났습니다. 나빠진 가독성과 과도해진 보일러 플레이트 코드 작성이었습니다.컴포넌트에서 useContext를 사용하려면 반드시 부모 노드에 provider가 있어야 합니다. 여러 컴포넌트들에서 useContext가 사용된다면 그들 간의 공통 부모 노드에 provider가 위치 해야 하고 경우에 따라 최상위 노드까지 provider의 위치가 올라가게 됩니다.팀에서 nextjs를 사용 중이었기 때문에 여러 페이지에 동일한 provider를 적용하는 일이 발생했고, 반복을 줄이기 위해 _app 파일로 provider를 올리면 getInitialProps가 비대해지는 문제가 발생했습니다.또한 페이지에 따라 불필요한 로직, provider가 _app 파일에 추가되는 것이니 성능적으로도 좋을 것이 없었고 이 코드가 현재 사용 중인지 로직을 따라가기에도 어려움이 생겼습니다. 적은 보일러 플레이트 코드를 가지고 여러 컴포넌트에서 전역 상태에 접근할 수 있는 무언가가 필요했습니다. 다시 상태 관리 라이브러리를 사용하면 해결되는 문제였지만 우리에게 필요한 기능에 비해 라이브러리가 컸고 이 정도 기능이라면 만들어서 써도 되지 않을까 하는 호기심에 vanilla-store를 만들게 되었습니다.내부 구현과 인터페이스 소개vanilla-store github linkvanilla-store는 스토어 생성 함수 createVanillaStore와 사용 hooks useStore로 구성되어 있습니다.createVanillaStore는 매개변수로 상태 값과 persist 옵션을 옵셔널로 받습니다. 이중 옵션은 아래에서 다루고 먼저 상태 값부터 보겠습니다const createVanillaStore = <State>(initialState: State, options?: Options<State>): VanillaStore<State> => { …}const initState = { count : 0, name : 'npay'}const store = createVanillaStore(initState)initialState는 store로 관리하려는 상태의 초깃값을 의미합니다. 이때 초깃값 선언과 createVanillaStore 실행은 컴포넌트 외부와 같이 반복적으로 실행될 여지가 없는 독립적인 환경에서 수행해야 합니다. 그래야 올바르게 초기화된 하나의 객체가 생성될 수 있기 때문입니다. createVanillaStore 내부를 좀 더 자세히 살펴보겠습니다.createVanillaStoreconst createVanillaStore = <State>(initialState: State, options?: Options<State>): VanillaStore<State> => { // 초기 상태값 할당 let state = initialState // set실행시 수행할 callback 함수 저장 set. 여기에 useSynExternalStore의 callback이 저장됨. const callbacks = new Set<() => void>() // 상태값 반환 함수. useSyncExternalStore의 snapShot으로 전달 const get = () => state // 상태값 변경 함수. useState처럼 상태값 혹은 함수를 받음 const set = (nextState: State | ((prev: State) => State)) => { // 서버에서의 사용을 허용하지 않는 가드문 if (typeof window === 'undefined') { throw new Error('This function is not available in Server side.') } // 상태값 혹은 함수 여부에 따라 다르게 state 값 갱신 state = typeof nextState === 'function' ? (nextState as (prev: State) => State)(state) : nextState // set에 저장되어있는 모든 callback 함수 실행. 이 동작으로 리액트 리렌더링이 트리거 callbacks.forEach((callback) => callback()) return state}… // useSyncExternalStore에 전달할 subscribe 함수 const subscribe = (callback: () => void) => { // useSyncExternalStore로 부터 전달받은 callback을 set자료구조에 저장 callbacks.add(callback) return () => { // cleanup 함수에서 등록했던 callback 제거 callbacks.delete(callback) } } return {get, set, subscribe, persistStore}}createVanillaStore는 상태 snapShot을 반환하는 get, 상태를 갱신하는 set, useSyncExternalStore에 전달할 subscribe, persist option 활성화 시 사용되는 persistStore 네 가지 값을 반환합니다.subscibe 함수는 useSyncExternalStore에 전달되어 컴포넌트 리렌더링을 트리거 하는 함수를 callback으로 받게 됩니다. 그리고 이를 callbacks 자료구조 Set에 등록하고 set 함수가 실행될 때마다 등록된 모든 callback 함수들을 실행합니다.즉, 상태 값이 바뀌는 set 함수가 실행될 때 컴포넌트 리랜더링을 트리거 하여 바뀐 상태로 형상을 갱신하게 됩니다. 이렇게 해서 외부 스토어와 리액트 간 싱크를 맞출 수 있게 됩니다.useStoreconst useStore = <State>(store: VanillaStore<State>, initialValue?: State) => { // useSyncExternalStore의 명세에 맞게 subscribe, snapShot을 전달하고 immutable한 상태값을 반환 const value = useSyncExternalStore(store.subscribe, store.get, () => initialValue || store.get()) … return [value, store.set] as const}useStore는 createVanillaStore에서 반환하는 subscribe, snapshot을 useSyncExternalStore에 전달하는 함수입니다. useSyncExternalStore를 통해 안전하게 관찰되는 상태 값과, 스토어에서 전달받은 set 함수를 반환합니다.기본 사용 예시// 초기 상태값 정의const initState = { count : 0, name : 'npay'}// createVanillaStore로 vanillaStore객체 생성const store = createVanillaStore(initState)export default function MyAppCount() { // useStore훅에 vanillaStore객체를 전달해 immutable한 상태값, setter함수를 반환 const [state, setState] = useStore(store) const handleClick = () => { // 상태값 변경 파라미터로 함수 전달. count 필드 값 변경 setState((prev) => ({…prev, count+1})) } return <div onClick={handleClick}>{state.count}</div>}export default function MyAppName() { const [state, setState] = useStore(store) const handleClick = () => { // name 필드 값 변경 setState((prev) => ({…prev, name:'point'})) } return <div onClick={handleClick}>{state.name}</div>}우리에게 익숙한 useState처럼 useStore를 통해 상태에 접근하고 변경할 수 있게 됩니다. 또한 useContext처럼 provider의 제약 없이 어떤 컴포넌트든 useStore를 통해 자유롭게 상태에 접근할 수 있습니다.다만 여기서 한 가지 아쉬운 부분이 생깁니다. 위 예시에서 MyAppCount 컴포넌트의 handleClick이 실행되면 count 값이 변경되면서 리렌더링이 트리거 될 것입니다.문제는 count를 사용하지 않는 MyAppName 컴포넌트 역시 store에서 관찰하는 상태가 변경되었기 때문에 마찬가지로 리렌더링이 트리거 됩니다. MyAppName 컴포넌트 입장에서는 불필요한 리렌더링이 실행되는 것이죠.이 문제를 해결하기 위해 vanilla-store에서는 useStoreSelector hooks를 준비했습니다.useStoreSelectorfunction useSyncExternalStoreWithSelector<Snapshot, Selection>( subscribe: (onStoreChange: () => void) => () => void getSnapshot: () => Snapshot, getServerSnapshot: undefined | (() => Snapshot), // 관찰하고 싶은 필드를 특정하기위한 필터링 함수 selector: (snapshot: Snapshot) => Selection, // 필드가 변경되었는지 비교하기위한 함수. 별도로 정의해주지 않으면 내장된 shallowEqual 함수로 비교연산 수행 isEqual: (a: Selection, b: Selection) => boolean = shallowEqual, ): Selection { // selector를 통해 전체 상태 값 중 관찰하고자하는 필드를 특정해 초기값 할당 const initialSelection = selector(getSnapshot()) // 위에 특정된 값을 리렌더링 이후 비교하기위해 useRef에 저장 const stateRef = useRef<Selection>(initialSelection) // useSyncExternalStore는 상태가 변경되면 변경된 상태값을 반환 const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) // useSyncExternalStore에서 반환된 상태값을 필터링 해 관찰하고 싶은 필드를 특정 const selection = selector(snapshot) // useSyncExternalStore 수행 전, 후 필드 값을 비교 if (!isEqual(selection, stateRef.current)) { // 만약 필드값이 변경되었다면 useRef 값 변경 stateRef.current = selection } // useRef의 current가 갱신된 경우에 반환값이 바뀌어 컴포넌트 리렌더링을 트리거 return stateRef.current}const useStoreSelector = <State, Value>( store: VanillaStore<State>, selector: (state: State) => Value, options?: {initialStoreValue?: State; isEqual?: (a: Value, b: Value) => boolean},) => { const {initialStoreValue, isEqual} = options || {} // useSyncExternalStore에 selector, isEqual만 파라미터로 추가됨 const value = useSyncExternalStoreWithSelector( store.subscribe, store.get, () => initialStoreValue || store.get(), selector, isEqual, ) return [value, store.set] as const}코드가 다소 복잡해 보이지만 내용은 간단합니다. useStore처럼 useSyncExternalStore를 통해 상태를 관찰하고 리렌더링을 트리거 하지만 selector를 통해 관찰하고자 하는 특정 필드를 선택하고 useRef를 통해 이를 저장합니다.이후 상태가 바뀌어 useSyncExternalStore의 반환값이 바뀌면 마찬가지로 selector를 통해 특정 필드를 선택하고 useRef의 값과 비교해 해당 필드가 변경된 경우에만 useRef의 값을 갱신합니다.이때 비교 함수 isEqual은 리액트의 shallowEqual처럼 동작하도록 구현했습니다. 비교하려는 필드가 중첩된 객체여도 key와 참조 값 value만을 비교할 뿐 하위 객체를 모두 순회해서 내부 필드가 바뀌었는지 까지는 판단하지 않습니다.조금 부정확할 순 있겠지만 비용이 큰 비교 연산을 절약해 성능상의 이점을 가져가기 위함입니다.useStoreSelector 사용 예시// 초기 상태값 정의const initState = { count : 0, name : 'npay'}// vanillaStore 객체 생성const store = createVanillaStore(initState)export default function MyAppCount() { // vanillaStore객체와 selector 전달. selector함수에서 count 필드를 관찰 // MyAppCount 컴포넌트에서는 전체 상태값 중 count 필드가 변경될 때에만 리렌더링을 트리거하게함 const [count, setState] = useStoreSelector(store, (state) => state.count) const handleClick = () => { // count의 상태값을 변경 setState((prev) => ({…prev, count+1})) } return <div onClick={handleClick}>{count}</div>}export default function MyAppName() { // selector함수에서 name 필드를 관찰 // MyAppName 컴포넌트에서는 전체 상태값 중 name 필드가 변경될 때에만 리렌더링을 트리거하게함 const [name, setState] = useStoreSelector(store, (state) => state.name) const handleClick = () => { // name의 상태값을 변경 setState((prev) => ({…prev, name:'point'})) } return <div onClick={handleClick}>{name}</div>}앞서 useStore를 사용했을 때 불필요한 리렌더링을 만들었던 부분을 useStoreSelector를 통해 개선한 모습입니다.useStoreSelector의 두 번째 인자로 selector 함수를 넘겨서 컴포넌트에서 관찰하려는 값을 한정하는 것을 볼 수 있습니다. 이제 MyAppName 컴포넌트는 count 상태 값이 바뀌어도 불필요한 리렌더링 수행하지 않게 됩니다!persist optionvanilla-store의 useStore에는 localStorage, sessionStoreage를 사용하는 persist option을 지원합니다. 이 기능은 페이지 이동, 새로고침을 해도 상태 값을 유지해야 하는 경우에 유용합니다.const createVanillaStore = <State>(initialState: State, options?: Options<State>): VanillaStore<State> => { // 상태값 초기화 및 할당 let state = initialState // localStorage or sessionStorage 저장소를 할당할 변수 let persistStore: Persistent<State> | null = null // 상태값 변경 시 수행할 callback함수 저장 set. // persistent옵션을 활성화한 경우 persistStore에 변경된 상태값을 할당하는 callback이 저장됨 const callbacks = new Set<() => void>()… // persist 옵션이 있는 경우에만 실행 if (options?.persist) { // persistent store에 저장된 value에 접근하기위한 key값 할당 const key = options.persist.key // 옵션에 전달한 타입이 localStorage인 경우 아래 조건문 실행 if (options.persist.type === 'localStorage') { // LocalStoragePersist 인스턴스 할당. 내부에 구현된 window.localStorage 사용 편의를 위해 만든 클래스 persistStore = new LocalStoragePersist(key, initialState, options.persist.typeAssertion) // callback으로 localStorage에 변경된 상태값을 반영하도록 하는 함수 저장. callbacks.add(() => { if (persistStore) { persistStore.value = get() } }) } // 옵션에 전달한 타입이 sessionStorage인 경우 아래 조건문 실행 if (options.persist.type === 'sessionStorage') { // SessionStoragePersist 인스턴스 할당. 내부에 구현된 window.sessionStorage 사용 편의를 위해 만든 클래스 persistStore = new SessionStoragePersist(key, initialState, options.persist.typeAssertion) // callback으로 sessionStorage에 변경된 상태값을 반영하도록 하는 함수 저장. callbacks.add(() => { if (persistStore) { persistStore.value = get() } }) } }… return {get, set, subscribe, persistStore}}// persist 옵션으로 type, key 전달// isStoreValue : 저장될 값 vanildation하는 함수. persistStorage에 저장하기 전 타입 에러 방지를 위해 검증하는 용도로 사용const store = createVanillaStore(initState, persist: {type: 'sessionStorage', key: 'sessionStorageKey', typeAssertion: isStoreValue})options에서 type을 통해 어떤 persist 저장소를 사용할지 선택하고 callbacks에 저장소 값을 변경하는 함수를 등록합니다. 이를 통해 상태가 변경되면 등록된 callback이 실행되어 컴포넌트 리렌더링 트리거와 함께 persist 저장소의 값을 갱신하게 됩니다.실제 사용하고 있는 서비스 예시vanilla-store는 네이버페이 내 자산에 수입 지출 서비스에 적용되어 있습니다. 수입 지출 서비스 내에서도 많은 부분들에 사용되고 있는데 이중 session storage를 사용하는 예시를 소개해드리려 합니다.수입 지출 서비스에는 여러 거래내역이 묶인 복합결제 거래내역이 있는데 클릭 시 세부 거래내역을 노출합니다. 거기에 새로고침을 해도 세부 내역 노출을 유지해야 하는 흔히 볼 수 있는 스펙입니다. 이를 useStore의 persist 옵션을 사용해 구현했습니다const sessionStorageKey = 'sessionStorageKey'type UnfoldStatus = Record<string, boolean>// 상태 초기 값. key로 거래내역의 id, value로 접힘여부를 판단하는 boolean값을 사용const initValue: UnfoldStatus = {}// vanillaStore persist 옵션 넣어서 생성export const unfoldStatusStore = createVanillaStore(initValue, { // sessionStorage 사용, sessionStorageKey를 key로 sessionStorage에 저장된 값 접근 persist: {type: 'sessionStorage', key: sessionStorageKey, typeAssertion: isStoreValue},})function ItemComponent() { const [status, setStatus] = useStore(unfoldStatusStore) …}해당 묶음 거래내역의 id를 key로 가지는 상태를 만들어 sessionStorage에 저장해 key가 있다면 세부 내역을 노출하도록 구현했습니다.복합결제 거래내역복합결제의 세부 내역까지 노출된 형상개발자 도구를 통해 클릭에 따라 sessionStorage 값이 바뀌는 것을 볼 수 있습니다.예시의 수입 지출 외에 네이버페이 내 자산의 다양한 서비스에도 vanilla-store가 적용되어 있습니다!다른 상태관리와 비교vanilla-store의 가장 큰 특징은 가벼움에 있습니다. 배경 자체가 기본 react api의 약간의 아쉬움을 보완하는 것에서 출발했기 때문에 간결한 전역 상태 관리라는 기능에 집중해 번들사이즈가 작습니다.bundle phobia를 통해 상태 관리 라이브러리 recoil, jotai 와 비교해 봤을 때 vanilla-store의 번들 사이즈가 훨씬 작음을 볼 수 있습니다.naverpay/vanilla-storerecoiljotai또한 직접 내부를 js로 구현했기 때문에 별도 package.json의 dependencies가 없습니다. 이 때문에 다른 패키지와의 의존성 문제에서 자유로울 수 있습니다.다만, 리액트 환경만을 지원하고 위에 소개한 useSyncExternalStore를 내장하고 있기에 peer dependencies로 react 18 조건을 가지고 있는 부분은 주의해야 합니다.앞으로 더 개발할 것들실 서비스에서 vanilla-store를 적용하면서 몇 가지 아쉬운 부분이 있었습니다. 개발 과정에서 스토어 내부의 상태를 추적하기 번거로웠던 것입니다. 지금의 vanilla-store는 값이 어떻게 바뀌는지 확인하려면 직접 console.log를 찍어봐야 알 수 있습니다.작은 규모로 사용할 때는 흐름을 따라가기 쉽지만 크고 복잡한 서비스에서는 이 부분이 아쉬울 수 있겠다는 생각을 했습니다. 그래서 persist만 있는 options에 디버깅 옵션을 추가할 예정에 있습니다.또 하나의 아쉬운 점은 ssr 지원을 하지 않는 것입니다. 저희 팀 서비스들은 대부분 nextjs의 ssr을 적극적으로 사용하고 있는데 vanilla-store는 처음엔 클라이언트 사이드에서만 사용하는 것을 상정하고 제작했기 때문에 ssr 시점에는 동작하지 않습니다.이 부분을 보완하기 위한 패키지 내부에서의 provider 제공 작업도 예정되어 있습니다. 위 예시들뿐만 아니라 vanilla-store를 사용하면서 부족한 부분들이 보이면 계속해서 보완해 나갈 예정입니다.마치며지금까지 vanilla-store에 배경과 구현, 실제 적용된 예시들을 살펴봤습니다. 서비스 스펙이 아닌 기술을 위한 개발을 해보고 실 서비스에 적용해 보는 건 색다른 재미를 줬던 것 같습니다. 특히 간단한 상태 관리 기능에만 집중하긴 했지만 생각보다 쉽게 기존의 상태 관리 라이브러리를 대체할 수 있다는 것이 신기했습니다.여기에는 몇 가지 이유가 있다고 생각하는데 기존에도 서비스에서 상태 관리 라이브러리의 일부 간단한 기능만을 사용했다는 점, useSyncExternalStore가 복잡해질 수 있는 리액트와 외부 저장소 간의 싱크 문제를 매우 간단하게 처리해 준 점입니다.저와 비슷한 고민을 하는 분이 있다면 vanilla-store가 작게나마 도움이 되었기를 희망하면서 글을 마치겠습니다. 네이버페이는 서비스 개발뿐만 아니라 다양한 기술적 도전들에 열려있습니다. vanilla-store도 팀에서 다양한 개발을 지원하고 독려하는 분위기가 있어서 끝까지 개발을 추진할 수 있던 것 같습니다. 이런 도전적 문화와 사람들 속에서 함께 성장할 동료를 찾고 있습니다! https://recruit.naverfincorp.com/상태 관리 라이브러리 vanilla-store was originally published in NAVER Pay Dev Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.

😲 "어? 올리브영에도 개발자가 있나요?" 🤔: 올리브네트웍스를 말씀하시는 건가요? 올리브영은 다 SI로 개발하는 거 아니었어요? 🫢: 우와, 올리브영에 개발자가 생각보다 많네요! 그것도 15…

안녕하세요. ABC Studio에서 Demaecan(出前館, 이하 데마에칸) 앱을 개발하고 있는 김종식입니다. 데마에칸은 2000년부터 서비스를 시작한 일본 최대 규모의 음식 배달...

최고의 자동완성 플러그인 타입스크립트를 사용하여 개발자를 속이고 코드를 최적화한 경험을 공유합니다.

목차 시작하며 규모가 커지면서 생기는 문제점들 2.1 계속해서 생겨나는 중복 코드들 2.2 멀티레포 환경에서의 공통 라이브러리 업데이트 일관성을 위한 모노레포 도입과 고민 프로젝트 세팅 비용 단축하기 4.1 기존 멀티레포 환경에서의 프로젝트 세팅 4.2 모노레포 환경 구성 세팅하기 4.3 Code Generator를 활용한 프로젝트 세팅 4.4 결론 ...

올리브영은 뷰티 상품을 중심으로 각기 다른 특징을 가진 상품을 고객에게 전달하고 있습니다. 최근엔 W CARE…

안녕하세요! 올리브영에서 라이브관과 매거진관을 담당하고 있는 몌으니입니다🦦💙 올리브영의 백오피스(BackOffice)시스템(이하 BO)에 Storybook…

Fail률 감소 목표 집요하게 달성하기 — Android UI 자동화안녕하세요. 29CM QA Engineer 홍해진입니다.29CM QA팀에서는 모바일 앱 배포전 BVT(Build Verification Test : 빌드 검증 테스트)로 UI 자동화를 진행하고 있습니다.Android UI 자동화를 만들며 겪었던 크고 작은 문제들 중 일부와 해결을 위한...

Figma는 기존의 다른 툴과 비교했을 때 가볍고 다양한 플러그인을 업무에 적용할 수 있다는 장점이 있습니다. 이 글은 디자이너 시각으로 Figma의 장점을 업무에 적용하는 과정에서 ChatGPT와 Figma 공식 문서를 참고해 Figma 플러그인을 제작한 경험을 공유합니다. The post Figma 플러그인, 디자이너가 직접 만들어 보기 appea...

안녕하세요. 리뷰커뮤니티 스쿼드의 백엔드 개발자 소보르빵🍞 입니다! 여러분은 올리브영 셔터를 알고 계시나요? 건강한 아름다움을 리딩하는 플랫폼 올리브영이 운영하는 뷰티 특화 커뮤니티 Shutter…

LINE 개발 조직에서는 성숙한 개발 문화를 만들기 위해 다양한 시도를 하고 있습니다. 클라이언트 앱 품질을 향상시키기 위해 개발 프로세스를 개선하고 있는 LY Mobile Dev...

안녕하세요. 올리브영에서 프론트엔드 개발 업무를 담당하는 코난입니다. 올리브영 프론트엔드는 NEXT.JS 프레임워크를 사용하여 웹 페이지를 개발하고 있습니다. NEXT.JS 프레임워크를, Vercel이나, AWS Amplify…

안녕하세요. 상품 스쿼드의 백엔드 개발자 벙개맨⚡️ 입니다. 이번에 개선된 상품 설명 영역은 상품 상세 페이지 내에 위치하여 제품에 대한 자세한 설명이 포함된 문서를 말하는데요. 저희 올리브영에서는 상품 설명을 생성할 때 이미지 타입과 HTML…

안녕하세요, ABC Studio에서 Demaecan(出前館, 이하 데마에칸) 앱을 개발하고 있는 김종식입니다. 데마에칸은 2000년부터 서비스를 시작한 일본 최대 규모의 음식 배달...

안녕하세요. 모바일앱개발팀 윌, 의지수입니다🙇♂️ 이번 글에서는 올리브영 앱의 바코드 스캔 성능을 개선한 경험을 공유하고자 합니다. 2024 APP뿐페스티벌 올리브영은 매년 옴니채널 활성화를 위해 APP뿐페스티벌을 진행합니다. 올해 APP…

안녕하세요. 사람인 개발팀 노혜민입니다. 이번 포스팅은 Vue3, Composition API와 Pinia를 이용한 상태관리 (1) 글의 후편입니다. 이전 포스팅에서 Composition API, Pinia에 대한 이론적인 설명을 다루었다면 이번 포스팅에서는 실제로 Pinia를 어떤 방식으로 적용했고 어떤 작업 결과를 냈는지 다루려합니다. 글의 목차는...

안녕하세요. 사람인 FE개발팀 지성봉입니다. 사람인 FE 개발팀에서는 기존의 사람인 서비스를 점진적으로 FE 분리 전환을 진행 중에 있는데요, 최근 사람인 서비스 중 신입·인턴 채용달력 모바일 서비스(이하 채용달력)를 React + TypeScript(이하 TS)로 전환하게 되었습니다. React + TS로의 전환은 제 개인적으로도 제법 작지 않은 도...

파이썬은 오래전부터 서버 프레임워크 언어로 쓰였고, 거의 모든 규모와 모든 환경의 프로젝트를 지원합니다. 그러나 전통적으로는 백엔드 영역에 갇혀 있었습니다. 프론트엔드, 클라이언트 측 코드를 만들기 위한 용도로 파이썬을 쓰는 문화는, 적어도 아직까지는 크게 확산하지 않았습니다. 새로운 파이썬 웹 프레임워크 제품군을 사용하면 백엔드에서 프로그래밍 방식으...

안녕하세요~ 올리브영 커머스 서비스 개발팀에서 프론트엔드개발을 하고 있는 개발새발자 입니다~ 만반잘부 👋🏼👋🏼👋🏼 이번 포스팅에서는 Next.JS에 MSW…

지난 글에 이어 이번 포스트 역시 Let'Swift 202…

…
안녕하세요. 뱅크샐러드 iOS 챕터의 김봉균입니다. 최근 iOS 챕터는 뱅크샐러드 iOS…

스켈레톤 로딩, 언제 사용해야 할까? — 헤이딜러 UX 스터디- ‘스켈레톤 로딩’을 어떤 기준으로 사용하기로 결정했는지에 대해 공유합니다- 그런데 용어는 Shimmer일까요? Skeleton일까요?안녕하세요.헤이딜러 안드로이드팀 박상권입니다.지금 이 글을 읽는 여러분들은 아래질문에 답하실 수 있으신가요?“스켈레톤 로딩은 어떤 화면에서 사용해야 할까?”...