본문 바로가기

[MobX, mobx-react-lite] Observable 다양하게 활용하기

현재 다니고 있는 회사에서는 MobX와 mobx-react-lite*을 활용하여 상태를 관리하고 있다.

 

크게 아래 세 과정에 의해 스토어를 생성하고 사용한다.

 

과정 자세히보기

1. 'create~Store' 네이밍 룰로 옵저버블로 만들고자 하는 객체를 생성한다.

const createAccountStore = () => {
      return {
        id: '',
        password: '',
        setId(value) {
          this.id = value
        }
        get isValidId() {
          return this.id.length > 5
        }
      }
    }

    export {createAccountStore}

 

2. mobx-react-lite의 useLocalStore*로 만든 옵저버블을 컨텍스트에 등록한다.

const rootContext = createContext()
const {Provider} = rootContext

const RootProvider = ({children}) => {
 return (
   <Provider
     value={{
       AccountStore: useLocalStore(createAccountStore)
     })
   >
   {children}
   </Provider>
 )
}

const App = () => {

  return (
    <RootProvider>
    {
      // ...
    }
    </RootProvider>
  )
}

 

3. useContext로 각 Store를 가져오는 hook을 생성하고 컴포넌트 뷰에서 hook으로 옵저버블을 사용한다.

export const useAccountStore = () => useContext(rootContext).AccountStore

const IdInput = () => {
  const {id, setId, isValidId} = useAccountStore()
  
  const handleChange = ({target}) => {
    const {value} = target
    
    setId(value)
  }
  
  return (
    <input type="text" id="user-id" value={id} onChange={handleChange} />
    { isValidId
     ? null
     : <p>5자리 이상 입력해주세요.</p>
    }
  )
}

 

mobx를 쓰면서 observable은 상태를 관리하기 위해서만 쓰이는 거야! 라는 생각이 고정되기 쉬운데

사내에서 스터디에 참가 + 다른 서비스에서 어떻게 쓰는지 보면서 이렇게 사용할수도 있구나 깨달았던 것들이 있다.

직접 만들어 써본 것도 기록할 겸 정리해본다.


* mobx-react-lite : mobx-react의 라이트 버전으로 함수형 컴포넌트만을 지원한다.

 

* useLocalStore : react hook을 통해 초기화 함수를 실행하고 컴포넌트 생명주기동안 이를 유지한다. 초기화 함수가 리턴하는 객체를 자동으로 observable로 만들고, getter는 computed로, 메서드는 action으로 bound해준다.

 

 

1. 외부에서 쓰이지 않는 private한 값들을 관리

개요 코드

import {action, observable} from 'mobx'

const createProcessStore = () => {
  // 스토어 내부에서 생성한 옵저버블
  const process = observable({
    percent: 0,
    complete: false,
  })
  
  const update = action((percent) => {
    scrapingProgress.percent = percent
    scrapingProgress.complete = percent >= 100
  })
    
  return {
    request() {
      // ...
      update(count * 100)
    },
    cancel() {
      // ...
      update(0)
    },
    get isComplete() {
      return process.complete
    }
  }
}

export {createProcessStore}

 

useLocalStore에 의해 생성되는 옵저버블은 위 규칙 상 hook을 호출하기만 하면 컴포넌트에서 접근이 가능하다.

 

그런데 경우에 따라서는 컴포넌트 뷰까지 나갈 필요는 없는 값들이 있다. 주로 스토어 내부에서만 사용하는 값들이 그러한데 이러한 값들을 따로 observable로 만들어 사용할 수 있다.

 

스토어 내부에서 외부의 간섭이 없는 private한 옵저버블을 생성하면 컴포넌트로 리턴되는 값의 로직 또한 간단해지는 장점이 있다.

 

또한 private한 observable를 computed로 참조하고 있으면, private observable이 변할 때 computed 또한 그 변화를 감지하여 최종적으로 computed를 참조하고 있는 컴포넌트의 렌더링을 유발할 수 있다.

 

2. 유틸성 옵저버블

개요 코드

 

버튼을 누르면 호출되는 api의 지연시간이 3~5초로 길어지면 사용자가 버튼을 연속해서 누르면 어떻게 될까?

 

사용자가 누른 횟수만큼 api가 다회 호출될 것이다.

 

위 문제를 해결하기 위해 api 호출 시 지연시간동안 pending시키는 기능을 구현하였다.

 

pending을 관리하는 유틸성 옵저버블(PendingList)을 구현하여 스토어 내부에서 클로저로 선언(createPendingList)하고,  api를 호출하기 전에 pending하는 로직을 추가하였다.

 

아래처럼 사용하는 위치에서 pending을 직접 설정/해제해도 되고, fetch 함수를 구현하여 자동으로 관리할 수 있도록 하였다.

 

usage 1.

// in SomeStore.js
const taskKey = '/task'
const pendingList = createPendingList(taskKey)

return observable({
  async task() {
    if (pendingList.isPending()) {
      return
    }

    pendingList.pend()
    const res = await axios.get('/task')
    pendingList.free()

    return res
  }
})

usage 2.

// in SomeStore.js
const taskKey = '/task'
const pendingList = createPendingList(taskKey)

return observable({
  async task() {
    // fetch의 두번째 인자는 options로 key와 pendingResponse를 설정할 수 있습니다.
    const res = await pendingList.fetch(() => axios.get('/task')) 
    return res
  }
})

 

유틸성으로 옵저버블을 사용하는 다른 경우를 하나 더 소개한다. (직접 구현한 건 아니지만...)

 

api에서 받아오는 데이터를 내부적으로 캐싱하여 캐싱된 데이터가 존재할 때에는 api 재호출을 하지 않는 fetcher함수를 작성할 수도 있다. (실제로 팀 내에서 팀원분께서 구현하신 코드를 볼 수 있었다.👍 recoill의 원리를 반영하셨다고 한다.)

 

3. 웹 스토리지의 변화를 감지

개요 코드

컴포넌트의 값을 보존하거나 일시적으로 저장하기 위해 외부 스토리지를 사용해야 할 때가 있다. (LocalStorage, SessionStorage 등)

 

스토리지를 사용하는 로직을 컴포넌트에서 작성하는 것보다 스토어를 활용하여 관리하면 컴포넌트는 스토리지에 대해 신경쓰지 않을 수 있다. (어디에 저장하는지 자체를 신경쓰지 않아도 될것이다.)

 

스토리지에 저장/삭제/조회해오는 것을 observable로 등록하면 StorageStore를 사용하는 스토어는 스토리지의 변화를 감지할 수 있다.

 

4. 다형성을 구현한 컬렉션

개요 코드

- 서점에서 모든 책 정보를 받아와서 관리하자. 책의 종류로는 교과서, 소설, 프로그래밍 책이 있다.

- 교과서는 어느 학교에서 쓰이는지, 그 학교가 공립학교인지에 대한 정보를 가지고 있어야 한다. (교과서만이 가지는 특징)

// 모든 서적의 Base Observable
const createBook = ({
  title,
  numOfPages,
  tableOfContents,
  author,
  price,
  stock = 0
}) => {
  return observable({
    title,
    numOfPages,
    author,
    price,
    stock,
    get tableOfContents() {
      return tableOfContents.map((content) => {
        const result = Object.assign({}, content)
        const {title, subtitle, startPage, endPage} = content
        
        if (!title) {
          result.title = ''
        }
        
        return result
      })
    },
    sell() {
      if (this.stock < 1) {
        return
      }
      
      this.stock -= 1
    },
    supplement() {
      this.stock += 1
    }
  })
}

// 교과서에 대한 정보를 다룸
const createTextbook = ({school, isStudent, ...rest}) => {
  const book = createBook({
    ...rest
  })
  
  return observable({
    school,
    get title() {
      return book.title
    },
    get author() {
      return book.author
    },
    get numOfPages() {
      return book.numOfPages
    },
    get price() {
      return isStudent
      ? Math.ceil(book.price / 80)
      : book.price
    },
    get tableOfContents() {
      return book.tableOfContents.map((content) => {
        return content.level = content.title.include('심화') ? 'HARD' : 'NORMAL'
      })
    },
    get isPublicSchool() {
      return publickSchoolList.includes(this.school)
    },
    sell() {
      book.sell()
    },
    supplement() {
      book.supplement()
    },
    get stock() {
      return book.stock
    }
  })
}

const createNovel = () => {
  return observable({
    // ... 중략 (소설에 관한 정보를 다룸) ...
  })
}

const createProgrammingBook = () => {} // ... 중략 ...
const createBookByType = (bookData) => {
  const {type} = bookData
  
  if (type === 'TEXTBOOK') {
    return createTextBook(bookData)
  }
  if (type === 'NOVEL') {
    return createNovel(bookData)
  }
  if (type === 'PROGRAMMING') {
    return createProgrammingBook(bookData)
  }
}

// useLocalStore로 등록되는 initializer
const createBookStore = () => {
  return {
    books: [],
    async getBookData(date) {
      const {data} = axios.get('/books', {
        params: {
          date
        }
      })
      
      this.books = data.map((book) => createBookByType(book))
    }
  }
}

 

클래스의 상속 개념*처럼 가장 상위 개념이 존재하고, (위 예시에서는 createBook으로 생성되는 Observable) 하위 Observable들이 상위 Observable의 정보를 받아와 자신의 데이터를 관리하는 Observable을 생성한다.


* 물론 상속으로 구현된건 아니다. 합성이 더 적절하다.

 

 

createBookStore에서 서버로부터 책들 정보를 가져와(getBookData) type에 따라 각각 다른 Observable을 생성하고 있다.(createBookByType)

 

예시 코드 도식화

이는 리팩터링 기법 중에서 '조건부를 다형성으로 만들기' 에 해당하기도 한다. 하위 Observable은 공통 특성을 지니는 동시에 자신만의 특성을 가지고 있을 수 있다.

 


 

요약

mobx와 mobx-react-lite에서 옵저버블을 아래 4가지 방법으로 다양하게 활용할 수 있다.

 

1. 외부에서 쓰이지 않는 private한 값들을 관리

2. 유틸성 옵저버블

3. 웹 스토리지의 변화를 감지

4. 다형성을 구현한 컬렉션

 

'Today I Learn > 프론트엔드' 카테고리의 다른 글

cypress를 써보자! (1)  (0) 2021.05.16
TDD로 서비스 개발하기  (2) 2021.04.18
/** IGNORE*/ 주석  (0) 2021.04.09
[리액트] Compound Component 활용하기  (0) 2020.09.27
[리액트] useAsync Hook with Suspense  (0) 2020.08.22