약속을 포착하여 자리 표시자 데이터 제공

최근에 저는 제품에 대한 피드백을 위해 API를 쿼리한 다음 해당 피드백을 지정된 채널에 게시하는 간단한 Slack 봇을 작성했습니다. 봇은 또한 사용자가 피드백의 일부가 실행 가능한지 여부에 대해 투표할 수 있도록 합니다(예: "이 강의에 오타가 있습니다"vs. "확인"또는 "코멘트 없음").



이것은 "Hack Day"프로젝트였기 때문에 초기 구현은 그 이름에 걸맞게 매우 해키했습니다. 투표는 서버에 저장되지 않았습니다. 사용자는 원하는 만큼 투표할 수 있습니다. POST/handle-vote 요청(지속성 계층으로서의 Slack 😂)과 함께 제공된 문자열을 수정하여 투표를 처리했습니다.

// text => 'Yes: 0 No: 0'
// value => 'yes' || 'no

function updateAttachmentText(text, value) {
  votes = text.split(' ')
  if (value === 'no') {
    votes[3] = parseInt(votes[3]) + 1
  } else if (value === 'yes') {
    votes[1] = parseInt(votes[1]) + 1
  }
  return votes.join(' ')
}

const updated = updateAttachmentText('Yes: 0 No: 0', 'yes')
// => 'Yes: 1 No: 0'


이 해킹된 작은 봇은 우리 제품 팀에 매우 유용한 것으로 밝혀졌습니다. 그러나 저는 그 어둡고 끔찍한 비밀을 알고 투표 데이터 저장을 위해 Redis를 사용하는 보다 강력한 버전을 작성하기로 결정했습니다. 한 사용자가 여러 번 투표합니다.

봇 자체는 크론 작업을 사용하여 채널에 새로운 피드백을 게시합니다. 업그레이드하는 동안 해당 피드백의 ID로 새 "빈"레코드를 생성하는 단계를 해당 스크립트에 추가했습니다.

const initialVotes = { votes: { yes: [], no: [] } }
redisClient.store(id, JSON.stringify(initialVotes))


사용자가 버튼을 누르면 서버는 요청을 받고 ID로 피드백을 찾고 사용자 ID를 올바른 목록('예' 또는 '아니요')에 추가한 다음 Redis 스토어에 다시 저장합니다. 사용자가 단 한 번만 투표할 수 있도록 몇 가지 논리를 수행합니다.

여기서 문제는 원래 봇의 메시지와 관련된 것입니다. 이러한 피드백 비트에는 애플리케이션의 ID와 연결된 레코드가 없습니다. 따라서 사용자가 투표 버튼을 클릭하면 다음 코드는 실패합니다.

// Imagine our Redis client setup...
class Redis {
  // setup, etc

  fetch(key) {
    return new Promise((resolve, reject) => {
      this.client.get(key, (err, response) => {
        if (err || !response) { 
          return reject('Unable to find result for', key, err) 
        }
        return resolve(response)
      })
    })
  }
}

// ... and a Vote loading class...
class Vote {
  constructor(id, redisClient) { 
    this.id = id 
    this.redisClient = redisClient 
  }

  loadVote() {
    return this.redisClient.fetch(this.id)
      .then(voteData => JSON.parse(voteData))
  }
}

const vote = new Vote(someId, someRedisClient)

vote.loadVote().then((vote) => incrementCounterAndSave(vote)) 
// Uncaught rejection :(


처음에는 이것이 내 서버 코드 어딘가에 존재하지 않는 레코드를 처리하기 위해 조건부 논리가 필요한 성가신 문제라고 생각했습니다. 하지만 Vote 클래스의 코드 자체를 보면 더 깔끔한 옵션이 나타납니다.

class Vote {
  // ...
  loadVote() {
    return this.redisClient.fetch(this.id)
      .then(voteData => JSON.parse(voteData))
      .catch((err) => {
        console.log('Encountered an error, returning placeholder data:', err)
        return { votes: { yes: [], no: [] } }
      })
  }
}

const vote = new Vote(someId, someRedisClient)

vote.loadVote()
  .then((vote) => {
    console.log(vote)
    incrementCounterAndSave(vote)
  }) 
// Encountered an error, returning placeholder data: 
//   'Unable to find result for someId (error here)
// { votes: { yes: [], no: [] } }


한동안 Promise와 함께 일하면서 이 개념이 내 첫 번째 직감이 아니었기 때문에 이 글을 써야겠다고 생각했습니다. catch를 체인의 맨 끝에서 사용할 생각은 없었습니다. then 통화 중.

다음은 이것을 매우 간단하게 보여주는 콘솔에서 놀 수 있는 몇 가지 코드입니다!

class DataFetcher {
  constructor() {
    this.count = 0
  }

  fetch() {
    this.count += 1
    return new Promise((resolve, reject) => {
      // Cause this Promise to be rejected/fail every other time the fetch function is called.
      this.count % 2 === 0 ? resolve('data from DB!') : reject('data not found')
    })
  }
}

const client = new DataFetcher()

const getData = () => {
  return client.fetch()
    .then((res) => {
      return res
    })
    .catch(err => {
      return 'placeholder data'
    })
}

getData.then(console.log) // placeholder data
getData.then(console.log) //data from DB!


참고로 async/await를 사용하여 덜 중첩된(아마도 더 읽기 쉬운) 방식으로 이 코드를 완전히 작성할 수 있습니다. 어느 쪽이든 그다지 강력하게 생각하지 않아서 Promise를 사용했습니다.

즐거운 코딩하세요!

좋은 웹페이지 즐겨찾기