영수증이나 신분증 같은 이미지 파일에서 텍스트를 손수 입력하는 건 정말 귀찮다. 많은 개발자들이 OCR(광학 문자 인식) 기능이 필요하다는 걸 알지만, 대부분 복잡한 라이브러리 설정이나 비용 때문에 망설인다. 다만 국내 개발자라면 네이버 클로바 OCR API를 몰랐을 가능성이 크다. 이번에는 네이버 클로바 OCR의 정확한 사용법, 한글 인식 강점, PHP에서의 실전 구현까지 완벽하게 정리해서 소개하겠다.
네이버 클로바 OCR은 머신러닝 기반의 문자 인식 서비스다. 이미지를 업로드하면 이 안에 있는 텍스트를 자동으로 추출해준다. 특히 한글 인식률이 뛰어나다는 게 가장 큰 장점이다. 구글의 클라우드 비전 API나 아마존 텍스트랙트 같은 해외 서비스들도 한글을 지원하지만, 한글 특화 모델인 클로바가 훨씬 정확한 결과를 준다.
네이버 클로바 OCR은 크게 두 가지 모드로 동작한다. 첫 번째는 문서 인식(Document OCR) 모드로, 영수증이나 신청서처럼 구조화된 문서를 인식한다. 두 번째는 일반 텍스트 인식 모드로, 사진에 담긴 모든 텍스트를 찾아낸다. 대부분의 실무에서는 문서 인식 모드를 쓴다.
클로바 OCR을 쓰려면 먼저 네이버 클라우드 콘솔에서 API 키를 발급받아야 한다. 네이버 계정이 없으면 만들고, 클라우드 콘솔에 접속한다(https://console.ncloud.com). 로그인 후 AI Naver API 메뉴로 들어가 클로바 OCR을 선택한다.
클로바 OCR에는 두 가지 플랜이 있다. 무료 플랜은 월 5000건, 유료 플랜은 더 많은 요청을 처리한다. 개발 초기 단계라면 무료 플랜으로 충분하다. 신청하면 바로 API 키(X-OCR-SECRET)와 엔드포인트 URL을 받는다. 이 정보를 안전한 곳에 저장해둬야 한다.
이제 실제로 PHP에서 이미지를 업로드하고 텍스트를 추출해보자. 기본 흐름은 이렇다. 1) 이미지 파일을 서버에 업로드한다. 2) 업로드된 파일을 클로바 OCR API로 전송한다. 3) 응답받은 JSON 데이터를 파싱해서 텍스트를 추출한다.
사용자가 이미지를 선택해서 업로드하면, PHP에서 $_FILES 배열로 받는다. 이때 보안을 위해 파일 확장자와 MIME 타입을 검증해야 한다.
<?php
// ✗ 잘못된 코드: 파일 타입 검증 없이 바로 업로드
$uploadDir = 'uploads/';
$fileName = $_FILES['image']['name'];
move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $fileName);
// 공격자가 .php 파일을 업로드할 수 있음
?>
<?php
// ✓ 올바른 코드: 파일 타입 검증 후 안전하게 저장
$uploadDir = 'uploads/';
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$allowedExt = ['jpg', 'jpeg', 'png', 'gif'];
// MIME 타입 검증
if (!in_array($_FILES['image']['type'], $allowedTypes)) {
die('jpeg, png, gif만 업로드 가능합니다');
}
// 확장자 검증
$fileExt = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (!in_array($fileExt, $allowedExt)) {
die('파일 확장자가 유효하지 않습니다');
}
// 새 파일명으로 저장 (충돌 방지)
$newFileName = uniqid('ocr_') . '.' . $fileExt;
$uploadPath = $uploadDir . $newFileName;
if (!move_uploaded_file($_FILES['image']['tmp_name'], $uploadPath)) {
die('파일 업로드 실패');
}
echo '파일 저장 완료: ' . $uploadPath;
?>
업로드된 파일을 클로바 OCR API로 보낸다. 이때 주의할 점은 HTTP multipart/form-data 형식으로 파일을 전송해야 한다는 것이다. cURL을 써서 구현한다.
<?php
// ✗ 잘못된 코드: multipart 형식 설정 누락
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://ocr.apigw.ntruss.com/custom/v1/...');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, ['image' => '@' . $uploadPath]);
// multipart_form_data로 자동 감지되지 않음
curl_exec($ch);
?>
<?php
// ✓ 올바른 코드: CURLFile로 multipart 형식 명시
define('OCR_SECRET', 'YOUR_OCR_SECRET_KEY');
define('OCR_ENDPOINT', 'https://ocr.apigw.ntruss.com/custom/v1/24821/document');
function sendToClova($imagePath) {
// 파일 존재 여부 확인
if (!file_exists($imagePath)) {
return ['error' => '파일을 찾을 수 없습니다'];
}
// CURLFile을 사용해 multipart 형식으로 명시
$cfile = new CURLFile($imagePath, mime_content_type($imagePath), 'document');
$postFields = [
'document' => $cfile,
'lang' => 'ko', // 한글 인식
'requestId' => uniqid(),
'resultType' => 'json'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, OCR_ENDPOINT);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-OCR-SECRET: ' . OCR_SECRET
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['error' => 'cURL 오류: ' . $error];
}
if ($httpCode !== 200) {
return [
'error' => '클로바 API 응답 오류',
'httpCode' => $httpCode,
'response' => $response
];
}
return json_decode($response, true);
}
// 사용 예
$result = sendToClova('uploads/ocr_12345.jpg');
var_dump($result);
?>
클로바 OCR이 반환하는 JSON 응답을 파싱해서 실제 텍스트를 추출한다. 응답 구조는 다음과 같다.
<?php
// 클로바 API 응답 예시
/*
{
"document": {
"pages": [
{
"index": 0,
"width": 1024,
"height": 768,
"tables": [],
"lines": [
{
"text": "회사명: ABC주식회사",
"confidence": 0.95,
"words": [...]
},
{
"text": "사업자등록번호: 123-45-67890",
"confidence": 0.92,
"words": [...]
}
]
}
]
},
"inferenceId": "abc123def456"
}
*/
// ✗ 잘못된 코드: 신뢰도 확인 없이 모든 텍스트를 사용
function extractText($clovaResponse) {
$extractedText = [];
foreach ($clovaResponse['document']['pages'] as $page) {
foreach ($page['lines'] as $line) {
$extractedText[] = $line['text'];
}
}
return $extractedText;
}
// 신뢰도가 낮은 오인식 텍스트도 포함됨
?>
<?php
// ✓ 올바른 코드: 신뢰도(confidence) 임계값으로 필터링
function extractText($clovaResponse, $minConfidence = 0.8) {
if (!isset($clovaResponse['document']) || !isset($clovaResponse['document']['pages'])) {
return ['error' => 'OCR 응답 형식이 유효하지 않습니다'];
}
$extractedLines = [];
foreach ($clovaResponse['document']['pages'] as $page) {
if (!isset($page['lines'])) continue;
foreach ($page['lines'] as $line) {
// 신뢰도가 임계값 이상인 텍스트만 추출
if (isset($line['confidence']) && $line['confidence'] >= $minConfidence) {
$extractedLines[] = [
'text' => trim($line['text']),
'confidence' => $line['confidence']
];
}
}
}
return $extractedLines;
}
// 테이블(표) 데이터 추출
function extractTables($clovaResponse) {
$tables = [];
foreach ($clovaResponse['document']['pages'] as $page) {
if (!isset($page['tables'])) continue;
foreach ($page['tables'] as $table) {
$rows = [];
if (isset($table['cells'])) {
foreach ($table['cells'] as $cell) {
$rows[$cell['rowIndex']][$cell['columnIndex']] = $cell['textSpan']['text'];
}
}
$tables[] = $rows;
}
}
return $tables;
}
?>
영수증 이미지를 업로드하면 날짜, 총액, 가맹점명 같은 정보를 자동으로 추출하는 실제 코드다.
<?php
define('OCR_SECRET', 'YOUR_OCR_SECRET_KEY');
define('OCR_ENDPOINT', 'https://ocr.apigw.ntruss.com/custom/v1/24821/document');
define('UPLOAD_DIR', 'receipts/');
class ReceiptOCR {
public function processReceipt($imagePath) {
// 1. 파일 검증
if (!$this->validateFile($imagePath)) {
return ['error' => '유효하지 않은 파일입니다'];
}
// 2. 클로바 API 전송
$clovaResponse = $this->sendToClova($imagePath);
if (isset($clovaResponse['error'])) {
return $clovaResponse;
}
// 3. 텍스트 추출
$extractedLines = $this->extractText($clovaResponse);
// 4. 영수증 정보 파싱
return $this->parseReceipt($extractedLines);
}
private function validateFile($path) {
return file_exists($path) && getimagesize($path) !== false;
}
private function sendToClova($imagePath) {
$cfile = new CURLFile($imagePath, mime_content_type($imagePath), 'document');
$postFields = [
'document' => $cfile,
'lang' => 'ko',
'requestId' => uniqid(),
'resultType' => 'json'
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => OCR_ENDPOINT,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_HTTPHEADER => ['X-OCR-SECRET: ' . OCR_SECRET],
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return ['error' => '클로바 API 오류 (' . $httpCode . ')'];
}
return json_decode($response, true);
}
private function extractText($clovaResponse, $minConfidence = 0.75) {
$lines = [];
if (isset($clovaResponse['document']['pages'])) {
foreach ($clovaResponse['document']['pages'] as $page) {
if (isset($page['lines'])) {
foreach ($page['lines'] as $line) {
if ($line['confidence'] >= $minConfidence) {
$lines[] = trim($line['text']);
}
}
}
}
}
return $lines;
}
private function parseReceipt($lines) {
$receipt = [
'storeName' => null,
'date' => null,
'total' => null,
'items' => []
];
foreach ($lines as $line) {
// 날짜 패턴: "2024-01-15" 또는 "24/01/15"
if (preg_match('/\d{2,4}[-\/]\d{1,2}[-\/]\d{1,2}/', $line, $matches)) {
$receipt['date'] = $matches[0];
}
// 금액 패턴: "12,345원" 또는 "12345"
if (preg_match('/(\d{1,3}(?:,\d{3})*|\d+)\s*원?/', $line, $matches)) {
$amount = str_replace(',', '', $matches[1]);
if ((int)$amount > 0) {
$receipt['total'] = (int)$amount;
}
}
// 가맹점명 (보통 첫 번째 긴 줄)
if (strlen($line) > 5 && $receipt['storeName'] === null) {
$receipt['storeName'] = $line;
}
}
return $receipt;
}
}
// 사용 예
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['receipt'])) {
$uploadPath = UPLOAD_DIR . uniqid('receipt_') . '.jpg';
if (move_uploaded_file($_FILES['receipt']['tmp_name'], $uploadPath)) {
$ocr = new ReceiptOCR();
$result = $ocr->processReceipt($uploadPath);
header('Content-Type: application/json');
echo json_encode($result);
}
}
?>
✗ 잘못된 방법 1. 모든 텍스트를 무조건 신뢰하기
클로바 OCR도 완벽하지 않다. 이미지 품질이 낮거나 손글씨가 섞여있으면 오인식한다. 신뢰도(confidence) 값을 반드시 확인해서 임계값 이상인 텍스트만 사용해야 한다.
✓올바른 방법. confidence 필터링 추가
0.8 이상의 신뢰도를 가진 텍스트만 추출하고, 더 중요한 필드는 0.9 이상으로 설정한다.
✗ 잘못된 방법 2. 타임아웃 설정 없이 API 호출
큰 이미지나 네트워크 지연이 있을 때 PHP 스크립트가 무한 대기에 빠질 수 있다. cURL의 CURLOPT_TIMEOUT은 반드시 설정해야 한다.
✓ 올바른 방법. 적절한 타임아웃 설정
OCR 처리는 시간이 걸리므로 30초 정도의 여유 있는 타임아웃을 설정한다. 또한 클라이언트 단에서도 별도의 처리 중 UI를 보여줘야 한다.
✗ 잘못된 방법 3. 파일 검증 스킵하기
사용자가 업로드한 파일을 직접 API로 보내면 악의적인 파일이 전송될 수 있다. 또한 API 요금도 낭비된다.
✓ 올바른 방법. 사전 검증 강화
파일 크기, MIME 타입, 이미지 해상도를 모두 검증한다. 보통 클로바 OCR은 최대 5MB 파일을 처리한다.
클로바 OCR API는 호출할 때마다 비용이 발생한다(무료 플랜도 월 한도가 있다). 같은 이미지로 중복 처리하지 않으려면 결과를 캐싱하자.
<?php
// 이미지 파일의 해시 값으로 캐시 키 생성
function cacheKey($imagePath) {
return 'ocr_result_' . md5_file($imagePath);
}
// 캐시 확인 후 처리
function processReceiptWithCache($imagePath, $redis = null) {
$key = cacheKey($imagePath);
// Redis에 캐시된 결과가 있는지 확인
if ($redis && $redis->exists($key)) {
return json_decode($redis->get($key), true);
}
// 없으면 새로 처리
$ocr = new ReceiptOCR();
$result = $ocr->processReceipt($imagePath);
// 결과를 캐시 (24시간 유효)
if ($redis && !isset($result['error'])) {
$redis->setex($key, 86400, json_encode($result));
}
return $result;
}
?>
네이버 클로바 OCR은 국내 개발자에게 정말 유용한 도구다. 한글 인식이 뛰어나고, API 연동도 직관적이며, 무료 플랜도 충분하다. 이 글의 검증, 타임아웃, 신뢰도 필터링 부분을 참고해서 안정적으로 구현하면, 영수증 관리나 신원증 인식 같은 실무 기능을 쉽게 만들 수 있을 것이다. 작은 자동화가 모여 큰 사용자 경험 개선을 만든다는 점을 잊지 말자.