๊ธฐ์กด์— ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ํ•˜๋˜ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งก๊ฒŒ ๋˜์—ˆ์ง€๋งŒ ๋งŽ์€ ๊ธฐ๋Šฅ๋“ค์ด ์ œ๋Œ€๋กœ ๊ตฌํ˜„๋˜์ง€ ์•Š์€ ์ฑ„๋กœ ๋ฐฉ์น˜๋˜์–ด ์žˆ์—ˆ๋‹ค. ๊ทธ ์ค‘ ์ด๋ฒˆ์— ํ•ด๋ณผ ์ฃผ์ œ๋Š” OCR์ด๋‹ค.

OCR์ด๋ž€?

OCR์€ Optical Character Recognition์˜ ์•ฝ์ž๋กœ, ๊ด‘ํ•™ ๋ฌธ์ž ์ธ์‹์˜ ์•ฝ์ž์ด๋‹ค. OCR์€ ์ด๋ฏธ์ง€์—์„œ ๋ฌธ์ž(ํ…์ŠคํŠธ)๋ฅผ ์ถ”์ถœํ•˜๋Š” ๊ธฐ์ˆ ๋กœ, ์Šค์บ”๋œ ๋ฌธ์„œ, ์‚ฌ์ง„, ์†๊ธ€์”จ ๋“ฑ์˜ ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•˜์—ฌ ๊ธฐ๊ณ„๊ฐ€ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ž ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ธฐ์ˆ ์ด๋‹ค.

OCR์˜ ์›๋ฆฌ

OCR์€ ํฌ๊ฒŒ ์ „์ฒ˜๋ฆฌ โ†’ ๋ฌธ์ž ์ธ์‹ โ†’ ํ›„์ฒ˜๋ฆฌ ๊ณผ์ •์œผ๋กœ ๋‚˜๋‰œ๋‹ค.

์ „์ฒ˜๋ฆฌ (Preprocessing)

์ด๋ฏธ์ง€๋ฅผ OCR๋กœ ๋ถ„์„ํ•˜๊ธฐ ์ „์— ์ตœ์ ํ™”ํ•˜๋Š” ๊ณผ์ •์ด๋‹ค. ํ•ด๋‹น ๊ณผ์ •์—์„œ๋Š”

  • ์ด์ง„ํ™”(Binary Thresholding): ์ปฌ๋Ÿฌ ๋˜๋Š” ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ ์ด๋ฏธ์ง€๋ฅผ ํ‘๋ฐฑ์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ช…ํ™•ํ•œ ์œค๊ณฝ์„ ์ƒ์„ฑ
  • ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ(Denoising): ๋ฐฐ๊ฒฝ ์ œ๊ฑฐ, ํ๋ฆฐ ์ด๋ฏธ์ง€ ๊ฐœ์„ , ๋ฌธ์ž์˜ ๋ช…ํ™•๋„ ์ฆ๊ฐ€
  • ๊ธฐ์šธ๊ธฐ ๋ณด์ •(Deskewing): ๋ฌธ์„œ๊ฐ€ ๊ธฐ์šธ์–ด์ง„ ๊ฒฝ์šฐ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ •๋ ฌ
  • ์˜์—ญ ๋ถ„ํ• (Segmentation): ๋ฌธ์„œ์—์„œ ํ…์ŠคํŠธ๊ฐ€ ํฌํ•จ๋œ ์˜์—ญ์„ ์ฐพ์•„ ๊ฐœ๋ณ„ ๋ฌธ์ž ๋˜๋Š” ๋‹จ์–ด๋กœ ๋ถ„ํ• 

๋“ฑ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

๋ฌธ์ž ์ธ์‹ (Character Recognition)

์ „์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚œ ์ด๋ฏธ์ง€์—์„œ ๋ฌธ์ž๋ฅผ ์ธ์‹ํ•˜๋Š” ๋‹จ๊ณ„๋กœ, ํŒจํ„ด ๋งค์นญ ๋ฐฉ์‹๊ณผ ๊ธฐ๊ณ„ ํ•™์Šต ๋ฐฉ์‹์ด ์žˆ๋‹ค.

  • ํŒจํ„ด ๋งค์นญ (Template Matching)
    • ๋ฏธ๋ฆฌ ์ €์žฅ๋œ ๊ธ€์ž ํŒจํ„ด๊ณผ ๋น„๊ตํ•˜์—ฌ ๋ฌธ์ž ์‹๋ณ„
    • ํฐํŠธ๊ฐ€ ์ผ์ •ํ•˜๊ณ  ๊ทœ์น™์ ์ธ ๋ฌธ์„œ์—์„œ ํšจ๊ณผ์ 
    • ๋‹ค์–‘ํ•œ ํฐํŠธ๋‚˜ ์†๊ธ€์”จ ์ธ์‹์—๋Š” ์•ฝํ•จ
  • ๊ธฐ๊ณ„ ํ•™์Šต (Machine Learning & Deep Learning)
    • ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ํ•™์Šตํ•œ ๋ชจ๋ธ์ด ๋ฌธ์ž๋ฅผ ํŒ๋ณ„
    • ๋”ฅ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜์˜ CNN(Convolutional Neural Network)๊ณผ RNN(Recurrent Neural Network)์„ ํ™œ์šฉํ•˜์—ฌ ๋ฌธ๋งฅ๊นŒ์ง€ ๋ถ„์„ ๊ฐ€๋Šฅ
    • ๋‹ค์–‘ํ•œ ํฐํŠธ ๋ฐ ์†๊ธ€์”จ ์ธ์‹์— ๊ฐ•ํ•จ

ํ›„์ฒ˜๋ฆฌ (Postprocessing)

์ธ์‹๋œ ํ…์ŠคํŠธ๋ฅผ ๋” ์ •์ œํ•˜๊ณ  ์ •ํ™•๋„๋ฅผ ๋†’์ด๋Š” ๊ณผ์ •์ด๋‹ค.

  • ๋ฌธ๋งฅ ๋ถ„์„(Context Analysis): ๋ฌธ์žฅ ๊ตฌ์กฐ๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ์˜ค๋ฅ˜ ์ˆ˜์ •
  • ์‚ฌ์ „ ๊ธฐ๋ฐ˜ ๋ณด์ •(Dictionary Correction): ๋ฌธ๋ฒ•์ ์œผ๋กœ ์ด์ƒํ•œ ๋‹จ์–ด๋ฅผ ์‚ฌ์ „์— ๋งž์ถฐ ์ˆ˜์ •
  • ๊ธ€์ž ์—ฐ๊ฒฐ ๋ฐ ๋„์–ด์“ฐ๊ธฐ ๋ณด์ •

๋“ฑ์˜ ๊ณผ์ •์„ ํ†ตํ•ด ํ…์ŠคํŠธ์˜ ํ’ˆ์งˆ์„ ๋ณด๋‹ค ๋†’์ด๋Š” ๊ณผ์ •์„ ๊ฑฐ์น˜๋ฉฐ ๊ฒฐ๊ณผ๋ฌผ์ด ๋‚˜์˜ค๊ฒŒ ๋œ๋‹ค.

Clova OCR

OCR ๋ชจ๋ธ์—๋„ ์—ฌ๋Ÿฌ ๊ฐ€์ง€๊ฐ€ ์žˆ์ง€๋งŒ, ์ด๋ฒˆ์— ์‚ฌ์šฉํ•ด๋ณผ ๊ฒƒ์€ NCP(Naver Cloud Platform)์˜ OCR ๋ชจ๋ธ์ด๋‹ค.

CLOVA OCR์€ ์ „์„ธ๊ณ„์ ์œผ๋กœ ๊ฐ€์žฅ ๊ถŒ์œ„ ์žˆ๋Š” ๊ธ€๋กœ๋ฒŒ ์ฑŒ๋ฆฐ์ง€์ธ ICDAR 2019 4๊ฐœ ๋ถ„์•ผ์—์„œ 1์œ„, CVPR ๋ฐ ICCV ๊ตญ์ œ ํ•™ํšŒ ๋…ผ๋ฌธ์œผ๋กœ ์„ ์ •๋˜๋Š” ๋“ฑ ๋…๋ณด์ ์ธ ๊ธฐ์ˆ ๋ ฅ์„ ์ž๋ž‘ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ์ฝ๋Š” ์ˆœ์„œ์™€ ๋ฐฉํ–ฅ์„ ์ถ”์ •ํ•ด ์ด๋ฏธ์ง€ ์† ๋ฌธ์ž๋ฅผ ์ธ์‹ํ•˜๋ฉฐ, ๊ณก์„ ์œผ๋กœ ๋ฐฐ์—ด๋˜๊ฑฐ๋‚˜ ๊ธฐ์šธ์–ด์ง„ ๋ฌธ์ž, ํ•„๊ธฐ์ฒด๊นŒ์ง€ ์ธ์‹ํ•  ์ˆ˜ ์žˆ์–ด ๋”์šฑ ์ •ํ™•ํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ผ๊ณ  ์„œ๋น„์Šค์˜ ์ •ํ™•ํ•œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ ๋Šฅ๋ ฅ์„ ๊ฐ•์กฐํ•œ๋‹ค.

์‚ฌ์‹ค ์“ฐ๊ฒŒ ๋œ ์ด์œ ๋Š” ๋‚ด๊ฐ€ NCP ํฌ๋ ˆ๋”ง์ด ๋งŽ์ด ๋‚จ์•„์„œ์ด๊ธฐ๋„ ํ•˜์ง€๋งŒ.. ์ถ”๊ฐ€์ ์œผ๋กœ Document OCR์ด๋ผ๋Š” ๊ธฐ๋Šฅ์„ ์ง€์›ํ•˜๋Š”๋ฐ, ํ•ด๋‹น ๊ธฐ๋Šฅ์€ ๋Œ€๋Ÿ‰์˜ ํ•™์Šต ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ CLOVA AI ๊ธฐ์ˆ ์„ ์ ์šฉํ•˜์—ฌ ํŠนํ™” ๋ฌธ์„œ์˜ ์ฃผ์š” ์ •๋ณด๋ฅผ ์ถ”์ถœํ•ด๋‚ด๋Š” ๋Šฅ๋ ฅ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. Document OCR์—๋Š” ์˜์ˆ˜์ฆ, ์‹ ์šฉ์นด๋“œ, ์‚ฌ์—…์ž๋“ฑ๋ก์ฆ ๋“ฑ๋“ฑ ํ‘œ์ค€ํ™”๋œ ๋ฌธ์„œ ์–‘์‹์— ๋Œ€ํ•ด์„œ ๋ฏธ๋ฆฌ document๋ฅผ ์ง€์ •ํ•˜๊ณ , ํ•ด๋‹น document์— ํŠนํ™”๋œ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์„œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณด๋‹ค ์‹ ๋ขฐ์„ฑ์ด ๋†’์„ ๊ฒƒ ๊ฐ™์•„์„œ ์„ ํƒํ•œ ์ด์œ ๋„ ์žˆ๋‹ค.

๋ฌผ๋ก  ๋ˆ์€ ๋‚ด์•ผํ•œ๋‹ค ๊ธฐ๋ณธ ์š”๊ธˆ์ด ์žˆ๋‹ค๋Š”๊ฒŒ ์‚ด์ง ํ‚น๋ฐ›์ง€๋งŒ, ๊ทธ๋งŒํผ์˜ ๊ฐ’์–ด์น˜๋ฅผ ํ•œ๋‹ค๋ฉด์•ผ ํฌ๋ ˆ๋”ง ์‚ฌ์šฉํ•ด๋„ ์ƒ๊ด€์€ ์—†์„ ๊ฒƒ ๊ฐ™๋‹ค.

Clova OCR์˜ Document OCR์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋ฏผ๊ฐ ์ •๋ณด๊ฐ€ ๋งŽ์€ document๋“ค์„ ์ฒ˜๋ฆฌํ•˜๋‹ค ๋ณด๋‹ˆ ์‚ฌ์ „ ์Šน์ธ์ด ํ•„์š”ํ•˜๋‹ค. ๋ฏธ๋ฆฌ ์‹ ์ฒญํ•ด๋†“๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค.

์‹ ์ฒญ์„ ํ•œ ๋’ค API Gateway๋„ ํ•˜๋‚˜ ์—ด์–ด ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.

๋‹ค๋ฅธ ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๊ฒ ์ง€๋งŒ, ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค์˜ API๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋จผ์ € ์š”์ฒญ ๋ฐ”๋””์™€ ์‘๋‹ต ์ฝ”๋“œ ๋“ฑ์— ๋Œ€ํ•ด์„œ ๋ฏธ๋ฆฌ ์ˆ™์ง€ํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. ํƒ€์ž…์„ ์ œ๊ณตํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ณด๋ฉด์„œ ํœด๋จผ ์—๋Ÿฌ๊ฐ€ ๋‚˜์ง€ ์•Š๋„๋ก ํ•ด์•ผํ•œ๋‹ค. ๋˜ํ•œ ๋จผ์ € postman ๋“ฑ์œผ๋กœ API๋ฅผ ํ…Œ์ŠคํŠธํ•œ ํ›„์— ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์‚ฌ์šฉํ•  ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ ์—๋Ÿฌ ์ฒดํฌํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค postman์—์„œ ๋ณด๋Š”๊ฒŒ ํ›จ ๋ณด๊ธฐ ํŽธํ•˜๋‹ค.

๊ทผ๋ฐ ์ด์ƒํ•œ๊ฒŒ requestId์— UUID ๋„ฃ์œผ๋ผ ํ•ด์„œ postman์—์„œ ์ง€์›ํ•˜๋Š” randomUUID๋ฅผ ๋„ฃ์—ˆ๋Š”๋ฐ 0011 ์ฝ”๋“œ๋กœ ๋œ ์—๋Ÿฌ๊ฐ€ ๋–ด๋‹ค ๊ณ„์† ์‚ฌ์ง„ ์˜ค๋ฅ˜์ธ๊ฐ€ ํ•˜๊ณ  ์ฐพ์•„๋ดค์ง€๋งŒ ๊ฒฐ๊ตญ requestId๋ฅผ ์œ„์™€ ๊ฐ™์ด UUID๊ฐ€ ์•„๋‹Œ ์ž„์˜์˜ ๊ฐ’์œผ๋กœ ํ•ด์ฃผ๋‹ˆ ๋˜์—ˆ๋‹ค(?) UUID๋ผ๊ณ  ํ•˜์ง€๋ฅผ ๋ง๋˜๊ฐ€.. ๊ทธ๋Ÿฌ๋‹ˆ ์ตœ๋Œ€ํ•œ API ์š”์ฒญ ์˜ˆ์‹œ๋ฅผ ๋ณด๊ณ  ๋”ฐ๋ผํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์€ ๋”ฐ๋ผํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค.

API ์„ค๊ณ„ํ•˜๊ธฐ - ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ๊ณ๋“ค์ธ

Postman์œผ๋กœ ํ…Œ์ŠคํŠธ๊นŒ์ง€ ๋˜์—ˆ๋‹ค๋ฉด ์‘๋‹ต ๊ฐ์ฒด๊ฐ€ ์–ด๋–ป๊ฒŒ ๋˜์–ด์žˆ๋Š”์ง€ clova api ๋ฌธ์„œ์™€ ๋น„๊ตํ•˜๋ฉด์„œ ํ™•์ธ๊นŒ์ง€ ๋˜์—ˆ์„ ๊ฒƒ์ด๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์ด์ œ ์ด api๋ฅผ ์–ด๋–ป๊ฒŒ ํ˜ธ์ถœํ•˜๊ณ , ์–ด๋–ป๊ฒŒ ์˜์ˆ˜์ฆ ์ธ์ฆ์„ ์•ผ๋ฌด์ง€๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„๊นŒ๋ฅผ ์ƒ๊ฐํ•ด๋ด์•ผ ํ•œ๋‹ค.

์›๋ž˜๋Œ€๋กœ api ํ˜ธ์ถœ์ด๋ผ ํ•จ์€

function receiptValidation() {
	const {data} = axios.post(API_URL, BODY);
	return data
}

์ด๋Ÿฐ ์‹์œผ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๊ฑฐ๋‚˜

function receiptValidation() {
	try{
		const {data} = axios.post(API_URL, BODY);
		return data
	}catch(error){
		throw error;
	}
}

์ด๋Ÿฐ ์‹์œผ๋กœ api ํ˜ธ์ถœ ํ•จ์ˆ˜์—๋„ try-catch๋ฌธ์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋‹ค.

ํ•˜์ง€๋งŒ http ์š”์ฒญ์„ ํ•˜๋Š” ํ•จ์ˆ˜์—์„œ try-catch๋ฌธ์„ ๊ฑธ๊ณ  ๋‹ค์‹œ ๋˜์ง€๋Š” ์ฝ”๋“œ๋Š” ๊ทธ๋ ‡๊ฒŒ ์ข‹์€ ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค. ์™œ๋ƒ๋ฉด ๋Œ€๋ถ€๋ถ„ ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ importํ•œ ๋’ค์— ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œํ‚ค๋Š” ๋ถ€๋ถ„์—์„œ๋„ try-catch๋ฅผ ํ†ตํ•ด ์—๋Ÿฌ ์ „ํŒŒ๋ฅผ ์žก์•„์ค˜์•ผ ํ• ํ…๋ฐ ๊ทธ๋ ‡๊ฒŒ ๋˜๋ฉด http ์š”์ฒญ๋ถ€์—์„œ ์‹คํ–‰๋œ ํ•จ์ˆ˜์˜ catch๋ฌธ์€ ๊ทธ์ € ๋ฐ›์•„์„œ ๋‹ค์‹œ ๋˜์ง€๋Š” ์—ญํ• ๋ฐ–์— ๋˜์ง€ ์•Š๋Š”๋‹ค. ๊ณต๋˜์ง€๊ธฐ ๋†€์ด๋ฅผ ๋‘๋ช…์ด์„œ ํ•˜๊ณ  ์žˆ๋‹ค๊ฐ€ ํ•œ๋ช…์ด ์‚ฌ์ด์— ๋ผ์–ด ๋“ค์–ด๊ฐ€์„œ ๊ณต์„ ๊ทธ์ € ๋ฐ›์•˜๋‹ค๊ฐ€ ๋‹ค์‹œ ์ฃผ๋Š”..๋А๋‚Œ์ด๋ž„๊นŒ..

๋ฌผ๋ก  ์ €๊ธฐ์—์„œ ๊ทธ์ € ๋˜์ง€๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ๊ฐ๊ฐ ์—๋Ÿฌ ๋กœ๊น…์„ ํ•ด์„œ callstack์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด์„œ๋ผ๋Š” ์šฉ๋„๋กœ๋„ ์“ฐ์ด๋Š” ๊ฒƒ ๊ฐ™์ง€๋งŒ ๋‚˜๋Š” ๊ตณ์ด ๊ทธ๋ ‡๊ฒŒ ํ•ด์•ผํ•˜๋‚˜ ์‹ถ์—ˆ๋‹ค.

์ถ”๊ฐ€์ ์œผ๋กœ ์—ฌ๊ธฐ์—์„œ๋Š” http ์š”์ฒญ์„ ๋‚ ๋ ธ์„ ๋•Œ, Bad Request๋ผ๋˜๊ฐ€ 400๋ฒˆ๋Œ€ ์—๋Ÿฌ์— ํ•ด๋‹นํ•˜๋Š” ๊ฒƒ๋“ค์€ ์—๋Ÿฌ๋กœ ๋Œ์•„์˜ค์ง€๋งŒ clova ocr์ด ์‚ฌ์ง„์„ ์ธ์‹ํ•˜๊ณ  ์‘๋‹ต์„ ๋ณด๋‚ด๋Š” ๊ณผ์ •์—์„œ ์‚ฌ์ง„์ด ์ œ๋Œ€๋กœ ์ธ์‹๋˜์ง€ ์•Š์€ ์—๋Ÿฌ์™€ ๊ฐ™์€ ๋ถ€๋ถ„์€ ์ •์ƒ์ ์ธ ์‘๋‹ต์˜ ์ด๋ฏธ์ง€ ๊ฐ์ฒด์˜ inferResult๋กœ ์˜ค๊ฒŒ ๋œ๋‹ค.

๋”ฐ๋ผ์„œ ์ด๋ ‡๊ฒŒ ์ œ๋Œ€๋กœ ์ธ์‹์ด ๋œ ๊ฒฝ์šฐ์™€ ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ๋ฅผ ๋‚˜๋ˆ„์–ด ๊ฐ๊ฐ ์—๋Ÿฌ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค˜์•ผ ํ–ˆ๋‹ค.

์ปค์Šคํ…€ ์—๋Ÿฌ๋กœ ์—๋Ÿฌ ํ•ธ๋“ค๋งํ•˜๊ธฐ

๋‚ด๊ฐ€ ์„ ํƒํ•œ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์€ axiosError์™€ ์ปค์Šคํ…€ ์—๋Ÿฌ๋ฅผ ํ˜ผํ•ฉํ•œ ๋ฐฉ์‹์ด๋‹ค. gateway api๋กœ ๋ณด๋‚ธ ์š”์ฒญ์—์„œ 400๋ฒˆ๋Œ€ ์—๋Ÿฌ๊ฐ€ ์˜ค๊ฒŒ ๋˜๋ฉด, 400๋ฒˆ๋Œ€ ์—๋Ÿฌ๊ฐ€ ์˜จ๋‹ค. ์ด๋Š” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ axiosError๋กœ catch์—์„œ ์žกํžˆ๊ฒŒ ๋œ๋‹ค.

ํ•˜์ง€๋งŒ ์œ„์—์„œ ๋งํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ์‘๋‹ต์€ ์ •์ƒ์ ์œผ๋กœ ์˜ค์ง€๋งŒ ์ด๋ฏธ์ง€ ์ธ์‹์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ๋ผ๋ฉด? ์ด ๊ฒฝ์šฐ์— ๊ทธ๋ƒฅ Error๋กœ ๋ณด๋‚ด๊ฒŒ ๋œ๋‹ค๋ฉด ์ด ์—๋Ÿฌ๊ฐ€ ์ด๋ฏธ์ง€ ์ธ์‹์— ์‹คํŒจํ•ด์„œ ๋‚˜๋Š” ์—๋Ÿฌ์ธ์ง€, ์ฝ”๋“œ๋‹จ์—์„œ ๋ญ๊ฐ€ ์ž˜๋ชป๋ผ์„œ ๋‚˜์˜ค๋Š” ์—๋Ÿฌ์ธ์ง€ ์ œ๋Œ€๋กœ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์‰ฝ์ง€ ์•Š๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์—๋Ÿฌ๋ฅผ ๋ณด๋‹ค ๋ช…ํ™•ํžˆ ํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋‚˜๋Š” ์ปค์Šคํ…€ ์—๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ๋‹ค.

class Error {
  constructor(message) {
    this.message = message;
    this.name = "Error"; // (name์€ ๋‚ด์žฅ ์—๋Ÿฌ ํด๋ž˜์Šค๋งˆ๋‹ค ๋‹ค๋ฆ…๋‹ˆ๋‹ค.)
    this.stack = <call stack>;  // stack์€ ํ‘œ์ค€์€ ์•„๋‹ˆ์ง€๋งŒ, ๋Œ€๋‹ค์ˆ˜ ํ™˜๊ฒฝ์ด ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
  }
}

๊ธฐ์กด ์—๋Ÿฌ๋Š” ์ด๋Ÿฐ ์‹์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋‹ค.

๋‚˜๋Š” ํ•ด๋‹น ์—๋Ÿฌ๋ฅผ ์ƒ์†๋ฐ›์•„ ์ƒˆ๋กœ์šด ์ปค์Šคํ…€ ์—๋Ÿฌ๋ฅผ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค. ๋ฌผ๋ก  ์—๋Ÿฌ๋ฅผ ๊ตณ์ด ์ƒ์†๋ฐ›์ง€ ์•Š์•„๋„ ๋˜์ง€๊ธฐ๋งŒ ํ•˜๋ฉด catch์—์„œ ์žกํžˆ๊ธด ํ•˜์ง€๋งŒ, ํ•ด๋‹นํ•˜๋Š” ๋ถ€๋ถ„์ด ์—๋Ÿฌ๋ผ๋Š” ๊ฒƒ์„ ๋ช…ํ™•ํžˆ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” Error์˜ stack๊ณผ ๊ฐ™์€ ์†์„ฑ๋“ค๋„ ๋ชจ๋‘ ์—๋Ÿฌ๋ฅผ ์ถ”์ ํ•˜๋Š”๋ฐ ํ•„์š”ํ•œ ์š”์†Œ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ƒ์†ํ•ด์ฃผ์—ˆ๋‹ค.

export class ValidationError extends Error {
    constructor(message: string) {
        super(message);
        this.name = 'ValidationError';
    }
}

๊ทธ๋Ÿผ ์ด์ œ ์ด๊ฑธ ๊ฐ€์ง€๊ณ  ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ? ๋จผ์ € ocr ํ•จ์ˆ˜์˜ ๊ฒ€์ฆ๋ถ€๋ฅผ ๋ณด์ž.

const REQUEST_ID = 'hospitalReceiptValidation';
const MIN_HOSPITAL_PRICE = 1000;
export const receiptValidationCheck = async (
    base64EncodedImage: string,
    hospitalName: string,
) => {
    const config = {
        headers: {
            'Content-Type': 'application/json',
            'X-OCR-SECRET': NAVER_CLOUD_PLATFORM_OCR_SECRET,
        },
    };
 
    const { data } = await axios.post(
        NAVER_CLOUD_PLATFORM_OCR_URL,
        {
            version: 'V2',
            requestId: REQUEST_ID,
            timestamp: Date.now(),
            images: [
                {
                    format: 'jpg',
                    name: 'ocrImage',
                    data: base64EncodedImage,
                },
            ],
            enableTableDetection: false,
        },
        config,
    );
 
    if (
        data.images[0].inferResult === 'FAILURE' ||
        data.images[0].inferResult === 'ERROR'
    )
        throw new ValidationError(
            '์ด๋ฏธ์ง€ ํ™•์ธ์ด ์ œ๋Œ€๋กœ ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์ฐ์–ด์ฃผ์„ธ์š”.',
        );
 
    if (
        data.images[0].receipt.result.storeInfo.name === hospitalName &&
        +data.images[0].receipt.result.totalPrice.price.formatted >= MIN_HOSPITAL_PRICE
    )
        return +data.images[0].receipt.result.totalPrice.price.formatted;
 
    throw new ValidationError('์˜์ˆ˜์ฆ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์ฐ์–ด์ฃผ์„ธ์š”.');
};

์—ฌ๊ธฐ์„œ base64Encoded ์ด๋ฏธ์ง€์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋Š” ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ Promise Chaining์„ ํ†ตํ•ด ๋„˜๊ฒจ์ฃผ๋Š” ๊ณผ์ •์—์„œ ์ง„ํ–‰๋˜๋ฉฐ, hospitalName์— ๋Œ€ํ•œ ์œ ํšจ๊ฐ’ ๋˜ํ•œ ๋ฏธ๋ฆฌ ๊ฒ€์ฆํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ๊ฒ€์ฆ์„ ํ•ด๋‹น ํ•จ์ˆ˜์—์„œ๋Š” ์‹œ์ผœ์ฃผ์ง€ ์•Š์•˜๋‹ค.

header์— ํ•„์š”ํ•œ ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๊ณ  post์— ํ•„์š”ํ•œ ๊ฐ’๋“ค์„ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  axios๋ฅผ ํ†ตํ•ด post ์š”์ฒญ์„ ๋ณด๋‚ด๊ฒŒ ๋˜๋ฉด data๊ฐ€ ์˜ฌ ๊ฒƒ์ด๋ผ ๊ฐ€์ •ํ•œ๋‹ค. data๊ฐ€ ์˜ค์ง€ ์•Š์•„ data is undefined์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋œจ๊ฒŒ ๋œ๋‹ค๋ฉด ํ•ด๋‹น ์˜ค๋ฅ˜๋Š” ๋น„์ •์ƒ์ ์ธ ์˜ค๋ฅ˜๋กœ ๊ฐ„์ฃผํ–ˆ๋‹ค. gateway api์ชฝ์—์„œ ๋ณด๋‚ด๋Š” ์˜ค๋ฅ˜๋Š” axiosError๋กœ ๋˜์ ธ์งˆ ๊ฒƒ์ด๊ณ  ๊ทธ๊ฒŒ ์•„๋‹ˆ๋ผ๋ฉด ๊ฐ์ฒด๊ฐ€ ๋ฌด์กฐ๊ฑด ์˜ค๊ฒŒ ๋˜์–ด์žˆ๋Š”๋ฐ ๋งŒ์•ฝ์— ์˜ค์ง€ ์•Š๋Š” ์˜ค๋ฅ˜๋Š” ๋น„์ •์ƒ์ ์ธ ์˜ค๋ฅ˜์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋”ฐ๋ผ์„œ ์ด๋Ÿฐ ์˜ค๋ฅ˜๋Š” ๊ทธ๋ƒฅ ์›์ธ์„ ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๋กœ ๋ณด๊ณ  catch๋‹จ์—์„œ ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๋ผ๊ณ  ๋‚ด๋ณด๋‚ด๊ฒŒ๋” ํ–ˆ๋‹ค.

๋‚ด๊ฐ€ ValidationError๋ฅผ ๊ฑด ๊ฒฝ์šฐ๋Š” ๋‘ ๊ฐ€์ง€ ๊ฒฝ์šฐ์ด๋‹ค.

  • image์˜ InferResult ๋กœ ์˜ค๋Š” ๊ฐ’์ด ERROR์ด๊ฑฐ๋‚˜ FAILURE์ผ ๋•Œ
    • ์ธ์‹์ด ์‹คํŒจํ–ˆ์„ ๋•Œ ์˜ค๋Š” ์˜ค๋ฅ˜๋ฅผ ์žก์•„์„œ ๋˜์ง„๋‹ค
    • ์ด ๊ฒฝ์šฐ์—๋Š” ์ธ์‹์ด ์ œ๋Œ€๋กœ ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์ด ๋ช…ํ™•ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์‹œ๊ธˆ ์‚ฌ์ง„์„ ์ฐ๋„๋ก ์œ ๋„ํ•˜๋ฉด ๋œ๋‹ค.
  • ๋ณ‘์› ์ด๋ฆ„๊ณผ ๊ฐ€๊ฒฉ์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ๋๋‚˜๊ณ ๋„ ์•„์ง return๋˜์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ
    • ํ•ด๋‹น ๊ฒฝ์šฐ๋Š” ๋ญ ์–ด๋–ป๊ฒŒ ๋˜๋“  ์ธ์‹ ํ›„์— ์ œ๋Œ€๋กœ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ๋ผ๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.
      • ๋ณ‘์›์—์„œ 1000์› ์ดํ•˜ ๋‚˜์™”์„ ๋•Œ
      • ๋ณ‘์› ์ด๋ฆ„์ด ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๋•Œ
    • ์˜ ๊ฒฝ์šฐ๊ฐ€ ์•„๋ž˜์˜ throw๋ฌธ์œผ๋กœ ๊ฐˆ ๊ฒƒ์ด๋‹ค. ์—ฌ๊ธฐ๊นŒ์ง€ ์˜ค๊ณ ๋„ ์•„๋ž˜๋กœ ๊ฐ€๊ฒŒ ๋˜๋ฉด ๊ฒ€์ฆ์ด ์ œ๋Œ€๋กœ ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ์˜๋ฏธ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋งˆ์ง€๋ง‰์— ValidationError๋ฅผ throw์‹œ์ผœ์ฃผ์—ˆ๋‹ค.

ํ•ด๋‹น ์˜ค๋ฅ˜๋“ค์€ ๋‚ด๊ฐ€ ์ธ์ง€ํ–ˆ๋˜ ์˜ˆ์™ธ์ƒํ™ฉ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ”์„ธ์ง€๋ฅผ ์ง์ ‘ ๋„ฃ์–ด์คŒ์œผ๋กœ์จ ํ•ด๋‹น ์—๋Ÿฌ์˜ ๋ฉ”์„ธ์ง€๋ฅผ ๊ทธ๋Œ€๋กœ ๋ชจ๋‹ฌ๋กœ ๋„์›Œ ๋ณด๋‚ด๋„ ๋˜๋„๋ก ํ•˜์˜€๋‹ค. ํ•˜์ง€๋งŒ ์ด์™ธ์˜ ๊ฒฝ์šฐ๋Š” ๋‚ด๊ฐ€ ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ–ˆ๋˜ ์˜ˆ์™ธ ์ƒํ™ฉ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ๋ถ€๋ถ„์˜ ์˜ค๋ฅ˜ ๋ฉ”์„ธ์ง€๋ฅผ ๊ทธ๋Œ€๋กœ ๋ชจ๋‹ฌ์— ๋„์›Œ์ฃผ๊ฒŒ ๋œ๋‹ค๋ฉด ์‚ฌ์šฉ์ž๋Š” ์ด๊ฒŒ ๋ฌด์Šจ ๋ง์ด์ง€? ์‹ถ์„ ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ ๋‹ค๋ฅธ ์—๋Ÿฌ์— ๋Œ€ํ•ด์„œ๋Š” ํ†ต์ผ๋œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ์•ˆ๋‚ด ๋ชจ๋‹ฌ์„ ๋„์›Œ์ฃผ๋ฉด ๋œ๋‹ค.

try {
	...
} catch (error) {
	if (error instanceof ValidationError) {
		pushError(error.message);
	} else if (axios.isAxiosError(error)){
		pushError(
			'์„œ๋ฒ„์™€์˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.',
		);
	} else{
		pushError(
			'์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ง€์†๋œ๋‹ค๋ฉด ๊ณ ๊ฐ์„ผํ„ฐ์— ๋ฌธ์˜ํ•ด์ฃผ์„ธ์š”.',
		);
	}
}

์ด๋Ÿฐ ์‹์œผ๋กœ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ฒŒ ๋˜๋ฉด ๋ณด๋‹ค ์„ธ๋ถ€์ ์œผ๋กœ ์—๋Ÿฌ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๊ณ , ์ด๋ฅผ ํ†ตํ•ด ๋ณด๋‹ค ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค!

ํšŒ๊ณ 

์‚ฌ์‹ค ์ด๊ฑด ๋ฆฌํŒฉํ† ๋ง์ด ์•„๋‹ˆ๋ผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ง  ์ฝ”๋“œ๊ฐ€ ์ข€ ๋” ๋งž๋Š” ํ‘œํ˜„๊ฐ™๊ธฐ๋„.. OCR์„ ๋„์ž…ํ•˜๊ณ , ๊ฒ€์ฆ ๊ณผ์ •์„ ๋‹ค์‹œ ๋ฆฌํŒฉํ† ๋ง ํ•˜๋ฉด์„œ ์ฝ”๋“œ๋ฅผ ๋ดค์„ ๋•Œ ์˜›๋‚ ์—๋Š” ์•„๋ฌด์ƒ๊ฐ์—†์ด ๊ทธ์ € ๋ณด๊ธฐ๋งŒ ํ–ˆ์—ˆ๋Š”๋ฐ ์ด์ œ๋Š” ๊ทธ๋ž˜๋„ ์ฝ”๋“œ์˜ ๋ญ๊ฐ€ ๋ถ€์กฑํ• ๊นŒ๋ฅผ ๋งŽ์ด ์ƒ๊ฐํ•˜๋ฉด์„œ ๋ณด๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

ํŠนํžˆ ์‹ค์ œ๋กœ ์šด์˜๋  ์„œ๋น„์Šค์ธ ๋งŒํผ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋Š” ์˜ˆ์ „์ฒ˜๋Ÿผ ๋ฌด์ง€์„ฑ์œผ๋กœ catch๋ฌธ์— ์ฝ˜์†”๋งŒ ์ฐ๋Š” ํ–‰์œ„๋ฅผ ํ•˜์ง€ ์•Š๊ฒŒ ๋˜์—ˆ๋‹ค. ์‚ฌ์šฉ์ž์—๊ฒŒ ๋˜ํ•œ ์˜ˆ์™ธ์— ๋Œ€ํ•ด ์•ˆ๋‚ด๋ฅผ ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ์ข‹์€ ์‚ฌ์šฉ์ž๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค ๋˜ํ•œ ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์œ ์ง€๋ณด์ˆ˜ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋‹ค๋ณด๋‹ˆ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ฐฉ์‹์„ ์ƒ๊ฐํ•˜๊ฒŒ ๋˜๊ณ , ์ด๋ ‡๊ฒŒ ๊ณ ๋ฏผํ•˜๋Š” ๊ณผ์ •์„ ํ†ตํ•ด ๋ณด๋‹ค ์ฝ”๋“œ์˜ ํ’ˆ์งˆ์ด ํ–ฅ์ƒ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

ํ•ญ์ƒ UX์™€ DX๋ฅผ ๋ชจ๋‘ ๊ณ ๋ คํ•˜๋ฉด์„œ ์ฝ”๋“œ๋ฅผ ์งœ๋„๋ก ์Šต๊ด€ํ™”ํ•˜์ž!