필요 지식
#node #async/await

이번엔 이전에 만들었던 ‘sharp 패키지로 이미지 자동 분할’ 하는 소스를 async/await 문법으로 변경해 볼 것입니다. 여기서 말하는 async는 node 의 async 패키지와 관련이 없는 문법적인 async 키워드가 되겠습니다.

async/await는 기본적으로 Promise 를 이용하는 문법입니다. 따라서 api 가 특정 형태를 제공하고 있다면 적용이 가능합니다. sharp 패키지는 콜백함수의 선언 유/무에 따라 Promise를 리턴하고 있으므로 async/await를 사용할 수 있습니다.

checkBlankPart() 수정하기

먼저 checkBlankPart() 함수를 보겠습니다.

이전 코드)

  1. function checkBlankPart(areaInfo, cb) {
  2. image.extract(areaInfo)
  3. .toBuffer((err, data, info) => {
  4. if(err) {
  5. console.log(areaInfo);
  6. throw new Error(err);
  7. }
  8. sharp(data).stats().then(({channels: [rc, gc, bc]}) => {
  9. //RGB 컬러의 평균값을 구한다
  10. const r = Math.round(rc.mean),
  11. g = Math.round(gc.mean),
  12. b = Math.round(bc.mean);
  13. if(r >= 230 && g >= 230 && b >= 230) { //255의 90% 값
  14. cb(true);
  15. return;
  16. }
  17. cb(false);
  18. });
  19. });
  20. }

sharp 패키지의 toBuffer() 함수는 Promise를 리턴하는 함수 입니다. 따라서 async/await 가 적용 가능하고 아래처럼 사용할 수 있습니다.

  1. let data = await image.extract(areaInfo).toBuffer();

이전 소스의 콜백함수 구조가 완전히 바뀌었습니다. 비동기 함수가 마치 동기 함수인것처럼 소스가 변했습니다. 그렇다고 toBuffer() 함수의 비동기 속성이 사라진것은 아닙니다. 여전히 비동기 함수이고 소스 코드 모양만 변한 것입니다.

stats() 함수 또한 Promise를 리턴할 수 있는 함수이고 따라서 아래와 같이 호출할 수 있습니다.

  1. let info = await sharp(data).stats();

await 키워드는 async로 정의된 함수 안에서만 사용 가능합니다. 그럼 함수에 async 키워드도 추가 해보겠습니다.

  1. async function checkBlankPart(areaInfo) {

그럼 함수 전체 모양은 어떻게 변했을까요?

  1. async function checkBlankPart(areaInfo) {
  2. let data = await image.extract(areaInfo).toBuffer();
  3. let info = await sharp(data).stats();
  4. let [rc, gc, bc] = info.channels;
  5. //RGB 컬러의 평균값을 구한다
  6. const r = Math.round(rc.mean),
  7. g = Math.round(gc.mean),
  8. b = Math.round(bc.mean);
  9. if(r >= 230 && g >= 230 && b >= 230) { //255의 90% 값
  10. return true;
  11. }
  12. return false;
  13. }

설명은 없었지만 checkBlankPart() 함수 또한 async/await로 바꿀 예정이기 때문에 콜백함수 정의도 없앴고 true/false도 콜백함수가 아니라 그냥 return 키워드를 이용하였습니다.

따라서 전체 소스 코드가 비동기 함수가 아닌것처럼 콜백함수 부분이 사라졌습니다. 이전 소스에 비해서 어떤가요? 논리적으로 훨씬 이해하기 쉬워졌나요? async/await를 사용하면 좋은 이유는 이렇게 소스가 정리될수 있다는 것 외에도 있지만 그것은 별도로 다뤄보겠습니다.

async 패키지 제거하기

checkBlankPart() 함수를 호출하는 부분을 바꿔보겠습니다.

  1. //이전 소스
  2. checkBlankPart({left:blankLeft, top:i, width:blankWidth, height:blankHeight}, function(isBlank) {
  3. });
  1. //변경 소스
  2. let isBlank = await checkBlankPart({left:blankLeft, top:i, width:blankWidth, height:blankHeight});

checkBlankPart() 함수는 Promise를 리턴하지 않는데 await 를 적용하였습니다. await 는 키워드 다음에 나오는 값이 Promise가 아닌 경우 해당 값을 resolved Promise 로 변경해주기 때문에 Promise 객체가 아니더라도 문제가 없습니다.

checkBlankPart() 함수에 await가 적용되면서 더 이상 async 패키지를 사용할 필요가 없어졌습니다. 그래서 우리에게 친숙한 for구문을 이용한 소스로 바꿀 수 있습니다. 아래 처럼요.

  1. for(let i = 0; i < imgHeight - blankHeight; i += blankHeight) {
  2. let isBlank = await checkBlankPart({left:blankLeft, top:i, width:blankWidth, height:blankHeight});
  3. if(isBlank) { //현재 위치가 여백 라인이라면
  4. let extractHeight = i - cropInfo.prevTop;
  5. if(!cropInfo.mode && extractHeight > 200) { //200픽셀보다 커야 의미있는 이미지
  6. cropInfo.mode = true;
  7. let option = {left:0, top:cropInfo.prevTop, width:imgWidth, height: extractHeight};
  8. console.log(option);
  9. image.extract(option).clone().toFile(i + ".jpg");
  10. }
  11. cropInfo.prevTop = i;
  12. }
  13. else
  14. cropInfo.mode = false;
  15. console.log(i);
  16. }

await 키워드는 async 키워드가 정의된 함수에서만 사용할수 있다고 했습니다. 이번엔 어느 부분에 추가하면 될까요? 최종 소스에서 확인해보세요.

정리

이번에는 이전에 만들었던 소스를 성능 개선 없이 async/await만 적용해 보았습니다. 아마 async/await 키워드가 익숙하지 않은 분이더라도 이 내용을 통해 얼마나 소스가 간결해졌는지 느끼실수 있을것 같습니다. 다음 내용에선 알고리즘이 아닌 기본 함수의 기능만으로 성능을 개선해보는 내용을 다루어 보겠습니다.

최종 소스

  1. const sharp = require("sharp");
  2. let imagePath = "sharp.jpg";
  3. async function checkBlankPart(areaInfo) {
  4. let data = await image.extract(areaInfo).toBuffer();
  5. let info = await sharp(data).stats();
  6. let [rc, gc, bc] = info.channels;
  7. //RGB 컬러의 평균값을 구한다
  8. const r = Math.round(rc.mean),
  9. g = Math.round(gc.mean),
  10. b = Math.round(bc.mean);
  11. if(r >= 230 && g >= 230 && b >= 230) { //255의 90% 값
  12. return true;
  13. }
  14. return false;
  15. }
  16. let start = new Date().getTime();
  17. let image = sharp(imagePath);
  18. image.metadata().then(async function(metadata) {
  19. let imgHeight = metadata.height; //이미지 높이
  20. let imgWidth = metadata.width; //이미지 넓이
  21. let blankWidth = imgWidth / 2; //여백인지 확인할 넓이
  22. let blankLeft = blankWidth / 2; //여백인지 확인할 위치
  23. const blankHeight = 5; //여백인지 확인할 높이
  24. let cropInfo = {
  25. mode : false,
  26. prevTop : 0
  27. };
  28. for(let i = 0; i < imgHeight - blankHeight; i += blankHeight) {
  29. let isBlank = await checkBlankPart({left:blankLeft, top:i, width:blankWidth, height:blankHeight});
  30. if(isBlank) { //현재 위치가 여백 라인이라면
  31. let extractHeight = i - cropInfo.prevTop;
  32. if(!cropInfo.mode && extractHeight > 200) { //200픽셀보다 커야 의미있는 이미지
  33. cropInfo.mode = true;
  34. let option = {left:0, top:cropInfo.prevTop, width:imgWidth, height: extractHeight};
  35. console.log(option);
  36. image.extract(option).clone().toFile(i + ".jpg");
  37. }
  38. cropInfo.prevTop = i;
  39. }
  40. else
  41. cropInfo.mode = false;
  42. console.log(i);
  43. }
  44. console.log("done");
  45. let end = new Date().getTime();
  46. console.log((end - start) / 1000);
  47. });