안전한 mock data 사용
프로젝트를 진행하다 보면 늘, 언제나 기획은 세밀화되고 살이 붙게 된다. 그리고 추가적인 작업들이 밀고 들어와 일정은 늘 지켜지지 못한다. 그래서 협업하는 사람들끼리 시작 시기가 맞지 않는 것이 일반적인데, 선후관계가 뚜렷한 작업에 대해서는 이게 좀 난감하다.
가장 난감한 것은 기획이 비어있는 것, 이건 시작 자체를 할 수가 없다. 기획 자체도 엉성하거니와 유관 부서에게 확인을 거치지 않은 뇌피셜인 경우가 흔해서 작업 일정을 통렬하게 갉아먹는다. 이 문제에서 바로 시안이 나오지 않는 상황이 이어진다. 기획이 비어있으니 시안이 나올 리 만무하다. 개발은? 당연히 시작하지도 못한다. 다행히 기획과 시안이 작업을 진행할 수 있을 만큼 올라와도 문제는 끝나지 않는다.
화면 구성에는 반드시 정보가 필요하다. 그리고 정보는 api를 통해 백엔드 서버로부터 전달받게 된다. api를 통해 얽혀있는 백엔드와 프론트엔드 작업이어서 백엔드가 먼저 시작한다고 생각하기 쉽지만 보통은 동시에, 혹은 어느 한쪽이 먼저 시작하게 된다. 이때 백과 프론트 양쪽은 모두 일정 부분 수정을 감수하며 작업을 병렬로 진행하게 된다. 백엔드가 먼저 시작하는 경우는 시안을 보고 필요한 정보를 미리 파악할 수 있으니 그 정보를 특정한 형태로 묶어 던져주면 어쨌건 그 정보를 받아 쓸 수 있다. 프론트엔드가 먼저 시작하는 경우에는 아직 받을 정보 구조가 확립이 안된 상황이기 때문에 시안에 기준하여 목(mock) 데이터를 만들고 추측성 퍼블리싱을 한다. 일단 목 데이터를 api 개발 측과 공유해도 그것이 ‘mock’ 형태 그대로 백엔드에서 넘어오지 않을 수도 있다. api 개발이 끝나면 양쪽의 세부 조정은 필수이다.
이번 작업도 기존 api를 참조한 추측성 목 데이터를 상태 관리 시스템의 dispatch 마지막에 산발적으로 넣어둔 채로 state에 강제 commit 시키며 작업하고 있었다. 대략 다음과 같았다.
async [SOME_DISPATCH](payload) {
try {
const { data } = await hollowAPI(payload)
commit(SOME_COMMIT, data)
} catch(error) {
const mockData = { ... }
commit(SOME_COMMIT, mockData)
console.error(error.response.message)
}
}
어차피 api가 정의되지 않았으므로 catch로 가게 되고 그곳에서 목 데이터를 정의하고 커밋해두면 되는 일이었다. 코드를 보고 기술 선임은 두 가지 제안을 했다.
1. 목 데이터를 json 파일로 관리하자.
2. 실제 api가 나올 때까지 비동기 요청의 형태를 유지해 줄 Promise 클로저 함수를 만들어 목 데이터를 사용한 곳에 일관되게 적용시켜두자.
두 제안 모두 합리적으로 보였다. 1, 2를 종합적으로 적용해두면 추후 api를 붙일 때 어느 곳에 작업을 해야 하는지, 어떤 것을 제거해야 하는지 한눈에 알아볼 수 있게 되어 후기 작업의 속도와 정확성이 높아지고 휴먼에러가 줄어들기 때문이다. 제안을 받아 들어 리팩토링 한 코드는 대략 다음과 같다.
interface IMockAPIOptions = { mockData: any, realAPIRequest: Promise | string }
const mockAPIPromise = (options: IMockAPIOptions) => {
const { mockData = {}, realAPIRequest = '' } = options
const mode = (typeof realApiRequest === 'string') ? 'mock' : 'real'
const invokeModeData = {
'mock': () => {
console.log(`TODO realAPIRequest 적용: ${realAPIRequest}`)
return mockData
}
'real': aync () => {
const { data } = await realApiRequest()
if(data 구조 !== IMockAPIOptions) {
throw error({message: 'Response interface is not matched to mock data'})
} else {
return data
}
}
}
return new Promise((resolve, reject) => {
try {
resolve(invokeModeData[mode]())
} catch(error) {
reject(error.response)
}
})
}
import mockData from './mock-data.json
[SOME_DISPATCH](payload) {
mockAPIPromise({ mockData, realAPIRequest: '/api/1.0/real-endpoint' })
.then(res => commit(SOME_COMMIT, res.data))
.catch(err => console.error(err.message)
}
물론 인터페이스에 억지스럽게 Promise | string이라는 타입을 정의해야 하지만 이렇게 해 둘 시 장점은 명확했다.
1. 추후 연동 작업 시 Inspector의 TODO 콘솔을 보면서 작업하면 절대 api 연결 작업을 놓칠 수 없다(물론 콘솔 메시지가 TODO라서 관습적으로 그냥 넘길 가능성을 배제할 순 없다. 꼭 히스토리는 공유하자).
2. 여러 dispatch의 catch에 흩뿌려져 있던 목 데이터도 하나의 json으로 관리되므로 추후 파악도 간편하다.
3. 실제 api response의 인터페이스가 mock에서 예상했던 바와 일치하는지에 대해서도 한번 더 확인하므로 백엔드 개발자와 api 명세에 대한 일치가 이루어졌는지를 검토할 수 있다.
여러모로 기술선임의 조언은 실제의 api가 나타나기 전 코드에 상당한 안정감을 주었다.
과학에서는 실제의 실험에 이르기 전 시뮬레이션을 돌려 이론이 예상대로 결과를 내뱉는지 확인한다. 시뮬레이션 결과 데이터는 물론 실제가 아니니 아무리 정교한 시뮬레이션에서 나왔다 하더라도 결국 '목 데이터'이다. 나의 이론이 맞는지는 결국 실제의 결과가 나와봐야 알 수 있다. 위 코드에서도 마찬가지, 실제의 브라우저 화면이 디자인 시안을 어느 정도 유사하게 구현했는지 실제 서버 api 데이터를 받아 확인해야 '실험'이 끝났다고 할 수 있다. 끝나기 전까지 시뮬레이션에서 실제의 실험 전 과정이 매끄럽게 이루어지도록 하기 위해 개발자는 실험 전체를 충분히 안전하고 유지보수 용이하게 제작해야 할 의무가 있다.