티스토리 뷰

Mobx에서 observable state를 변경하려면 항상 액션을 통해서 변경하는것을 추천한다. 그래야 Mobx가 state변경 사항을 모아서 한번에 처리해줄 수 있기 때문이다. @action 데코레이터나 action() 함수는 현재 실행중인 함수에만 적용된다. 현재 함수에 의해서 스케쥴된 함수에는 액션이 적용되지 않는다. 이게 무슨말인지 자세히 살펴보자.

// state가 항상 action으로만 변경되게끔 설정하는 옵션이다.
mobx.configure({ enforceActions: "observed" }) 

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        
        // 아래 Promise의 then에 전달되는 콜백 내부에는 action으로 묶이지 않는다.
        // 왜냐면 현재 실행중인 함수 내부가 아니라 스케쥴된 함수의 내부이기 때문이다.
        fetchGithubProjectsSomehow().then(
            projects => {
                const filteredProjects = somePreprocessing(projects)
                this.githubProjects = filteredProjects
                this.state = "done"
            },
            error => {
                this.state = "error"
            }
        )
    }
}

함수가 스케쥴된다는건 해당 함수가 비동기 처리 된다는 뜻이다.

브라우저에서 비동기 처리는 web api를 통해 함수가 이벤트루프 큐를 거쳐 콜스택에서 실행되는 구조이다.(링크)

 

이렇게, 현재 콜스택에서 실행되고 있는 함수에서만 action이 유효한것이지, 현재 함수에서 스케쥴한(실행 예정인) 함수 내부는 action이 유효하지 않다. 따라서, 그 내부에서도 한번더 action으로 감싸줘야 한다.

 

위 예제를 실행하면 에러가 난다.

// state가 항상 action으로만 변경되게끔 설정하는 옵션이다.
mobx.configure({ enforceActions: "observed" }) 

우리가 이 옵션을 켜줬기 때문에 observable state는 무조건 action내부에서만 변경되어야 한다. 근데 우리는 위 예제에서

fetchGithubProjectsSomehow().then(
  projects => {
  	// 스케쥴링된 콜백 함수 내부에서 observable state를 변경하고 있음.
    // 이 부분은 액션을 통해 observable state를 수정한것이 아님!
    const filteredProjects = somePreprocessing(projects)
    this.githubProjects = filteredProjects
    this.state = "done"
  },
  error => {
      this.state = "error"
  }
)

이렇게 스케쥴링된 함수 내부에서 observable state를 변경해주고 있기 때문이다. (action에서 변경한게 아니라)

 

올바른 방법

mobx.configure({ enforceActions: "observed" })

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        
        // 1. 스케쥴링될 콜백 함수를 분리한다음
        fetchGithubProjectsSomehow().then(this.fetchProjectsSuccess, this.fetchProjectsError)
    }

	// 2. 액션으로 묶어줬다.
    @action.bound
    fetchProjectsSuccess(projects) {
        const filteredProjects = somePreprocessing(projects)
        this.githubProjects = filteredProjects
        this.state = "done"
    }

	// 2. 액션으로 묶어줬다.
    @action.bound
    fetchProjectsError(error) {
        this.state = "error"
    }
}

이렇게 콜백을 액션으로 묶어주면 에러가 나지 않는다. 액션에서 observable state를 변경했기 때문에 정상 동작한다.

action.bound에서 bound는 함수 내부에서 this를 해당 클래스의 인스턴스를 가리키기 위한 this 바인딩이다.

좀 더 깔끔한 방법

물론 위 코드 처럼 작성해도 잘 동작하지만 그럼 매번 비동기 처리를 할 때마다 함수를 계속 만들어줘야 하는 불편함이 있다. 좀 더 간편한 방법을 알아보자.

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        
        // 스케쥴링 될 콜백함수 내부에서
        fetchGithubProjectsSomehow().then(

		// 인라인으로 액션을 생성함
            action("fetchSuccess", projects => {
                const filteredProjects = somePreprocessing(projects)
                this.githubProjects = filteredProjects
                this.state = "done"
            }),
            
		// 인라인으로 액션을 생성함
            action("fetchError", error => {
                this.state = "error"
            })
        )
    }
}

좀 더 깔끔깔끔한 방법

위 코드 처럼 작성해도 동작하지만, 액션에 이름을 계속 붙여줘야 하는 불편함이 여전히 존재한다. 또한, 타입스크립트가 액션의 타입을 추론하지 못하기 떄문에 콜백에 계속 타입을 적어줘야하는것도 불편하다.

 

이떄 runInAction을 사용하자.

mobx.configure({ enforceActions: "observed" })

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        
        // 스케쥴링될 콜백 함수 내부에서
        fetchGithubProjectsSomehow().then(
            projects => {
                const filteredProjects = somePreprocessing(projects)
                
                // 액션을 만들고 바로 실행하는 mobx의 유틸성 함수인
                // runInAction을 활용하자.
                runInAction(() => {
                    this.githubProjects = filteredProjects
                    this.state = "done"
                })
            },
            error => {
                // the alternative ending of this process:...
                runInAction(() => {
                    this.state = "error"
                })
            }
        )
    }
}

이 유틸 함수를 사용하면 액션을 매번 네이밍할 필요도 없고 타입을 적어줄 필요도 없어서 매우 간편하다.

 

async/await에서는?

mobx.configure({ enforceActions: "observed" })

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    async fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        
        try {
        	// async await은 마치 동기적으로 작동하는것처럼 보이지만
            // 사실은 promise를 쉽게 사용하게 해주는 syntax sugar(문법설탕)이다.
            // 그래서 액션 내부의 첫번째 await에서만 action이 적용된다.
            const projects = await fetchGithubProjectsSomehow()
            const filteredProjects = somePreprocessing(projects)
            
            // 액션이 적용된 함수 내부이므로 runInAction으로 감싸주지 않아도 동작할듯하지만,
            // 감싸줘야 한다. 왜냐면, 위에 await이 있기 때문이다.
            runInAction(() => {
                this.state = "done"
                this.githubProjects = filteredProjects
            })
        } catch (error) {
            runInAction(() => {
                this.state = "error"
            })
        }
    }
}

정리하자면, async/await 에서 action은 첫번째 await까지만 적용된다.

 

비동기 처리에서 액션을 가장 깔끔하게 처리 하는 방법, flow

mobx에서 제공하는 flow를 사용하면 위에서 비동기 처리 할때 첫번째 await까지만 액션이 적용되는 문제를 해결 할 수 있다.

mobx.configure({ enforceActions: "observed" })

class Store {
    @observable githubProjects = []
    @observable state = "pending"

	// async 대신 *(generator)를 사용하자.
    fetchProjects = flow(function*() {
        this.githubProjects = []
        this.state = "pending"
        try {
            const projects = yield fetchGithubProjectsSomehow() // await대신 yield
            const filteredProjects = somePreprocessing(projects)
          
          	// 이 부분이 자동으로 action으로 감싸진다.
            this.state = "done"
            this.githubProjects = filteredProjects
        } catch (error) {
            this.state = "error"
        }
    })
}

이렇게 async대신 *, await대신 yield를 넣어주기만 하면 mobx가 알아서 observable state를 변경하는 부분을 action으로 감싸줄 수 있다.

 

참고로, flow는 오직 함수로만 제공되며 decorator버전은 제공되지 않는다. 무조건 위 방식대로만 사용해야 한다. 또한 flow는 mobx 개발자 도구와도 잘 어울리므로 더더욱 좋다.

 

 

 

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함