이미지 로딩 중...
AI Generated
2025. 11. 9. · 2 Views
AI 음성 비서 만들기 완벽 가이드
음성 인식부터 자연어 처리, 응답 생성까지 실전 음성 비서 시스템을 구축하는 방법을 배워봅니다. Web Speech API와 AI 모델을 활용하여 실무에서 바로 적용 가능한 음성 비서를 만들어봅니다.
목차
- 음성 인식 기초 - Web Speech API로 시작하기
- 음성 명령 파싱 - 의도 분석하기
- 파라미터 추출 - 명령에서 값 뽑아내기
- 음성 응답 생성 - Text-to-Speech
- 대화 컨텍스트 관리 - 상태 유지하기
- AI 모델 통합 - GPT API 연동하기
- 웨이크워드 감지 - "헤이 비서" 구현하기
- 에러 처리와 폴백 - 안정성 확보하기
- 다국어 지원 - 글로벌 음성 비서 만들기
- 음성 명령 큐잉 - 연속 명령 처리하기
1. 음성 인식 기초 - Web Speech API로 시작하기
시작하며
여러분이 웹 애플리케이션에서 사용자의 음성을 텍스트로 변환하고 싶을 때 어떻게 시작해야 할지 막막했던 적 있나요? 복잡한 외부 API를 연동하거나 유료 서비스를 고민하다가 포기한 경험이 있을 수 있습니다.
이런 고민은 실제로 많은 개발자들이 겪는 문제입니다. 음성 인식은 어렵고 복잡할 것 같지만, 사실 브라우저에 내장된 Web Speech API를 사용하면 놀라울 정도로 간단하게 구현할 수 있습니다.
바로 이럴 때 필요한 것이 SpeechRecognition API입니다. 이 API를 사용하면 단 몇 줄의 코드로 사용자의 음성을 실시간으로 텍스트로 변환할 수 있습니다.
개요
간단히 말해서, Web Speech API는 브라우저에서 음성 인식과 음성 합성 기능을 제공하는 JavaScript API입니다. 이 API가 필요한 이유는 명확합니다.
사용자 경험을 향상시키고, 접근성을 개선하며, 핸즈프리 인터페이스를 제공할 수 있기 때문입니다. 예를 들어, 운전 중에 메시지를 작성하거나, 시각 장애인을 위한 음성 명령 시스템 같은 경우에 매우 유용합니다.
기존에는 외부 API(Google Speech API, Amazon Transcribe)를 연동하고 비용을 지불해야 했다면, 이제는 브라우저만으로 무료로 음성 인식 기능을 구현할 수 있습니다. 이 API의 핵심 특징은 실시간 스트리밍 인식, 다양한 언어 지원, 그리고 interim results(중간 결과) 제공입니다.
이러한 특징들이 사용자에게 즉각적인 피드백을 제공하고 자연스러운 대화형 인터페이스를 만들 수 있게 해줍니다.
코드 예제
// SpeechRecognition 객체 생성 (브라우저 호환성 처리)
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
// 음성 인식 설정
recognition.lang = 'ko-KR'; // 한국어 설정
recognition.continuous = true; // 연속 인식 모드
recognition.interimResults = true; // 중간 결과 표시
// 음성 인식 결과 처리
recognition.onresult = (event) => {
const transcript = Array.from(event.results)
.map(result => result[0].transcript)
.join('');
console.log('인식된 텍스트:', transcript);
};
// 음성 인식 시작
recognition.start();
설명
이것이 하는 일: 이 코드는 사용자의 음성을 실시간으로 감지하여 텍스트로 변환하고, 그 결과를 처리할 수 있는 기본적인 음성 인식 시스템을 구축합니다. 첫 번째로, SpeechRecognition 객체를 생성하는 부분이 핵심입니다.
브라우저마다 구현이 다를 수 있어서 window.SpeechRecognition과 window.webkitSpeechRecognition을 모두 확인합니다. 이렇게 하는 이유는 Chrome과 Safari 등 다양한 브라우저에서 호환성을 보장하기 위해서입니다.
두 번째로, 인식 설정을 구성하는 단계입니다. lang 속성으로 언어를 지정하면 해당 언어에 최적화된 인식 모델을 사용합니다.
continuous: true를 설정하면 사용자가 말을 멈춰도 인식이 계속되어 자연스러운 대화가 가능합니다. interimResults: true는 음성 인식 중간 과정의 결과도 받아볼 수 있게 해서 실시간 피드백을 제공할 수 있습니다.
세 번째로, onresult 이벤트 핸들러가 실행되면서 인식된 결과를 처리합니다. event.results는 여러 개의 인식 결과를 담고 있는 배열이며, 각 결과의 가장 확률이 높은 텍스트(result[0].transcript)를 추출하여 결합합니다.
마지막으로 start() 메서드를 호출하여 음성 인식을 시작하면 사용자의 마이크 입력을 받아들이기 시작합니다. 여러분이 이 코드를 사용하면 별도의 서버나 외부 API 없이도 웹 애플리케이션에서 음성 입력을 받을 수 있습니다.
실시간 자막 생성, 음성 검색, 음성 메모 등 다양한 기능을 구현할 수 있으며, 사용자에게 더 직관적이고 편리한 인터페이스를 제공할 수 있습니다.
실전 팁
💡 recognition.continuous = false로 설정하면 한 번의 발화 후 자동으로 인식이 중단되므로, 버튼을 누를 때마다 한 문장씩 입력받는 UI에 적합합니다.
💡 에러 처리를 위해 recognition.onerror 이벤트를 반드시 구현하세요. 마이크 권한 거부, 네트워크 오류 등 다양한 에러 상황을 처리할 수 있습니다.
💡 모바일 환경에서는 사용자 제스처(버튼 클릭 등) 후에만 음성 인식을 시작할 수 있으므로, 자동 시작 기능은 피하세요.
💡 recognition.maxAlternatives = 5를 설정하면 여러 개의 인식 후보를 받아볼 수 있어 정확도를 높이거나 사용자에게 선택권을 줄 수 있습니다.
💡 인식 정확도를 높이려면 조용한 환경에서 사용하도록 안내하고, recognition.stop()과 recognition.start()를 적절히 활용하여 불필요한 소음을 차단하세요.
2. 음성 명령 파싱 - 의도 분석하기
시작하며
여러분이 음성 비서를 만들 때 가장 어려운 부분이 무엇인지 아시나요? 사용자가 "오늘 날씨 어때?", "지금 날씨 알려줘", "오늘 비 오나?" 같은 다양한 표현으로 같은 질문을 할 때, 이를 모두 같은 의도로 인식하는 것입니다.
이 문제는 실제 음성 비서 개발에서 핵심적인 과제입니다. 자연어는 매우 다양하고 애매모호하기 때문에, 단순한 키워드 매칭으로는 한계가 있습니다.
사용자 의도를 정확히 파악하지 못하면 엉뚱한 답변을 하게 되어 사용자 경험이 크게 떨어집니다. 바로 이럴 때 필요한 것이 Intent Recognition(의도 인식)입니다.
패턴 매칭과 키워드 분석을 통해 사용자의 진짜 의도를 파악할 수 있습니다.
개요
간단히 말해서, 의도 인식은 사용자의 자연어 입력에서 핵심 키워드와 패턴을 추출하여 어떤 행동을 원하는지 파악하는 과정입니다. 이 기능이 필요한 이유는 음성 비서가 다양한 표현 방식을 이해하고 적절히 응답하기 위해서입니다.
예를 들어, 스마트홈 제어 시스템에서 "불 켜줘", "조명 켜", "라이트 온" 모두 같은 동작을 실행해야 하는 경우에 필수적입니다. 기존에는 if-else 문으로 모든 경우의 수를 하드코딩했다면, 이제는 패턴 기반 매칭과 정규표현식을 사용하여 확장 가능한 시스템을 만들 수 있습니다.
핵심 특징은 유연한 패턴 매칭, 키워드 기반 분류, 그리고 파라미터 추출입니다. 이러한 특징들이 사용자가 자연스럽게 말해도 정확히 이해할 수 있는 음성 비서를 만들 수 있게 해줍니다.
코드 예제
// 의도와 패턴 정의
const intents = [
{
name: 'weather',
patterns: ['날씨', '기온', '비', '눈', '맑'],
action: 'getWeather'
},
{
name: 'timer',
patterns: ['타이머', '알람', '시간 맞춰', '분 후'],
action: 'setTimer'
},
{
name: 'light',
patterns: ['불', '조명', '라이트', '켜', '꺼'],
action: 'controlLight'
}
];
// 의도 인식 함수
function recognizeIntent(text) {
const lowerText = text.toLowerCase();
for (const intent of intents) {
// 패턴 중 하나라도 매칭되면 해당 의도 반환
const matched = intent.patterns.some(pattern =>
lowerText.includes(pattern)
);
if (matched) {
return {
intent: intent.name,
action: intent.action,
originalText: text,
confidence: calculateConfidence(text, intent.patterns)
};
}
}
return { intent: 'unknown', confidence: 0 };
}
// 신뢰도 계산
function calculateConfidence(text, patterns) {
const matchCount = patterns.filter(p => text.includes(p)).length;
return matchCount / patterns.length;
}
설명
이것이 하는 일: 이 코드는 사용자가 말한 텍스트를 분석하여 어떤 행동을 원하는지 파악하고, 그에 맞는 액션과 신뢰도를 함께 반환하는 의도 인식 시스템입니다. 첫 번째로, intents 배열에 각 의도별로 이름, 매칭 패턴, 실행할 액션을 정의합니다.
각 의도는 여러 개의 패턴 키워드를 가질 수 있어서 다양한 표현 방식을 모두 커버할 수 있습니다. 예를 들어 '날씨' 의도는 "날씨", "기온", "비" 등의 키워드를 모두 포함하여 "오늘 날씨 어때요?"나 "비 올까요?" 같은 다양한 질문을 인식할 수 있습니다.
두 번째로, recognizeIntent 함수가 실행되면서 입력된 텍스트를 소문자로 변환하여 대소문자 구분 없이 매칭합니다. 그 다음 각 의도의 패턴들을 순회하면서 some 메서드로 하나라도 매칭되는지 확인합니다.
이렇게 하는 이유는 사용자가 "불 좀 켜줘"라고 말했을 때 "불"이나 "켜" 중 하나만 매칭되어도 조명 제어 의도로 인식할 수 있기 때문입니다. 세 번째로, 매칭이 성공하면 의도 이름, 실행할 액션, 원본 텍스트와 함께 신뢰도를 계산하여 반환합니다.
calculateConfidence 함수는 전체 패턴 중 실제로 매칭된 패턴의 비율을 계산합니다. 여러 개의 패턴이 매칭될수록 신뢰도가 높아지므로, 더 확실한 의도 파악이 가능합니다.
여러분이 이 코드를 사용하면 사용자가 어떤 방식으로 말하든 핵심 의도를 파악할 수 있습니다. 새로운 의도를 추가할 때도 intents 배열에 객체만 추가하면 되므로 확장성이 뛰어나며, 신뢰도 점수를 활용하여 애매한 명령에 대해서는 재확인을 요청하는 등의 고급 기능도 구현할 수 있습니다.
실전 팁
💡 정규표현식을 사용하면 더 복잡한 패턴 매칭이 가능합니다. 예: /타이머.*(\d+)분/으로 "5분 타이머 설정"에서 숫자를 추출할 수 있습니다.
💡 여러 의도가 동시에 매칭될 때는 신뢰도가 가장 높은 것을 선택하거나, 사용자에게 확인을 요청하는 로직을 추가하세요.
💡 자주 사용되는 명령은 패턴 배열의 앞쪽에 배치하여 매칭 속도를 최적화할 수 있습니다.
💡 동의어 사전을 별도로 관리하면 "TV"와 "티비", "텔레비전"을 같은 것으로 인식할 수 있어 인식률이 크게 향상됩니다.
💡 사용자 입력 로그를 수집하여 인식 실패한 패턴을 분석하고 지속적으로 패턴 데이터베이스를 업데이트하세요.
3. 파라미터 추출 - 명령에서 값 뽑아내기
시작하며
여러분이 음성 비서에게 "5분 후에 알람 맞춰줘"라고 말했을 때, 비서는 단순히 알람 설정 의도만 파악하면 될까요? 아닙니다.
"5분"이라는 구체적인 시간 값을 추출해야 제대로 동작할 수 있습니다. 이런 파라미터 추출은 실무에서 매우 중요합니다.
의도만 파악하고 구체적인 값을 추출하지 못하면 사용자에게 다시 물어봐야 하고, 이는 대화 흐름을 끊어서 사용자 경험을 해칩니다. "날씨"를 물어봤는데 어느 도시인지, "타이머"를 설정하려는데 몇 분인지 파악하지 못하면 비서로서의 가치가 크게 떨어집니다.
바로 이럴 때 필요한 것이 Entity Extraction(개체명 추출)입니다. 정규표현식과 패턴 매칭으로 문장에서 핵심 정보를 정확히 뽑아낼 수 있습니다.
개요
간단히 말해서, 파라미터 추출은 사용자의 자연어 명령에서 숫자, 날짜, 시간, 위치 등의 구체적인 값을 찾아내는 과정입니다. 이 기능이 필요한 이유는 명확합니다.
음성 비서가 실제로 동작하려면 의도뿐만 아니라 구체적인 값들이 필요하기 때문입니다. 예를 들어, "서울 날씨 알려줘"에서 '서울', "내일 오전 9시에 알람"에서 '내일'과 '9시', "볼륨 50으로 조절"에서 '50' 같은 값들을 추출해야 합니다.
기존에는 복잡한 문자열 파싱과 분기문으로 값을 추출했다면, 이제는 정규표현식 기반의 체계적인 추출 시스템을 만들어 재사용성과 유지보수성을 높일 수 있습니다. 핵심 특징은 정규표현식 기반 매칭, 타입별 추출 규칙, 그리고 기본값 처리입니다.
이러한 특징들이 복잡한 자연어에서도 정확하게 필요한 정보를 추출할 수 있게 해줍니다.
코드 예제
// 파라미터 추출 규칙 정의
const extractors = {
number: {
pattern: /(\d+)/g,
transform: (match) => parseInt(match[1])
},
duration: {
pattern: /(\d+)\s*(분|초|시간)/,
transform: (match) => ({
value: parseInt(match[1]),
unit: match[2]
})
},
location: {
pattern: /(서울|부산|대구|인천|광주|대전|울산|세종|제주)/,
transform: (match) => match[1]
},
time: {
pattern: /(\d+)시(?:\s*(\d+)분)?/,
transform: (match) => ({
hour: parseInt(match[1]),
minute: match[2] ? parseInt(match[2]) : 0
})
}
};
// 파라미터 추출 함수
function extractParameters(text, paramTypes) {
const params = {};
for (const paramType of paramTypes) {
const extractor = extractors[paramType];
if (!extractor) continue;
const match = text.match(extractor.pattern);
if (match) {
params[paramType] = extractor.transform(match);
}
}
return params;
}
// 사용 예시
const command = "서울 날씨 알려주고 30분 후에 알람 맞춰줘";
const params = extractParameters(command, ['location', 'duration']);
// { location: '서울', duration: { value: 30, unit: '분' } }
설명
이것이 하는 일: 이 코드는 사용자의 자연어 명령에서 시간, 숫자, 위치 등 다양한 타입의 파라미터를 자동으로 찾아내고 적절한 형태로 변환하는 추출 시스템입니다. 첫 번째로, extractors 객체에 각 파라미터 타입별로 추출 규칙을 정의합니다.
각 타입은 pattern(정규표현식)과 transform(변환 함수)으로 구성됩니다. 예를 들어 duration 타입은 "30분", "2시간" 같은 표현을 찾아내는 패턴을 가지고 있으며, 매칭되면 숫자와 단위를 분리하여 객체로 변환합니다.
이렇게 분리해두면 나중에 "분"을 초로 변환하거나 "시간"을 분으로 변환하는 등의 처리가 쉬워집니다. 두 번째로, extractParameters 함수가 실행되면서 요청된 파라미터 타입들을 순회합니다.
각 타입에 해당하는 extractor를 가져와서 text에서 패턴 매칭을 시도합니다. 정규표현식의 match 메서드를 사용하면 매칭된 전체 문자열과 캡처 그룹들을 배열로 받을 수 있습니다.
이 매칭 결과를 transform 함수에 전달하여 적절한 형태로 변환합니다. 세 번째로, 변환된 값을 params 객체에 파라미터 타입을 키로 하여 저장합니다.
모든 요청된 타입에 대한 추출이 완료되면 최종 params 객체를 반환합니다. 예시에서 보듯이 "서울 날씨 알려주고 30분 후에 알람 맞춰줘"라는 복잡한 명령에서도 location과 duration을 정확히 추출할 수 있습니다.
여러분이 이 코드를 사용하면 사용자가 자연스럽게 말한 명령에서 필요한 모든 정보를 자동으로 추출할 수 있습니다. 새로운 파라미터 타입을 추가할 때도 extractors 객체에 규칙만 추가하면 되므로 확장이 매우 쉽고, 타입별로 적절한 변환 로직을 적용하여 후속 처리가 편리한 형태로 데이터를 가공할 수 있습니다.
실전 팁
💡 정규표현식의 g 플래그를 사용하면 여러 개의 파라미터를 한 번에 추출할 수 있습니다. 예: "사과 3개랑 바나나 5개 주문"에서 모든 숫자 추출.
💡 추출 실패 시 기본값을 설정하는 로직을 추가하세요. 예: 위치가 없으면 사용자의 현재 위치를 사용.
💡 한국어 숫자("삼십분")도 처리하려면 별도의 변환 함수를 만들어 숫자 표현을 아라비아 숫자로 바꾸는 전처리 단계를 추가하세요.
💡 상대적 시간("내일", "모레", "다음 주")을 처리하려면 moment.js나 day.js 같은 라이브러리를 활용하여 절대 시간으로 변환하세요.
💡 파라미터 검증 로직을 추가하여 비정상적인 값(예: 25시, 100분)을 걸러내고 사용자에게 올바른 입력을 요청하세요.
4. 음성 응답 생성 - Text-to-Speech
시작하며
여러분의 음성 비서가 사용자의 명령을 완벽하게 이해하고 처리했다면, 이제 어떻게 대답해야 할까요? 화면에 텍스트만 표시하는 것은 진짜 음성 비서라고 할 수 없습니다.
음성으로 응답하지 않으면 사용자는 계속 화면을 봐야 하므로 핸즈프리의 장점이 사라집니다. 운전 중이거나 요리 중처럼 손을 사용할 수 없는 상황에서는 더욱 그렇습니다.
또한 시각 장애인 사용자에게는 음성 응답이 필수입니다. 바로 이럴 때 필요한 것이 Speech Synthesis API입니다.
텍스트를 자연스러운 음성으로 변환하여 사용자에게 들려줄 수 있습니다.
개요
간단히 말해서, Speech Synthesis API는 텍스트를 음성으로 변환하는 브라우저 내장 API로, 다양한 목소리와 언어를 지원합니다. 이 API가 필요한 이유는 완전한 음성 대화 경험을 제공하기 위해서입니다.
예를 들어, 스마트 스피커처럼 음성으로 질문하고 음성으로 답변을 듣는 자연스러운 인터페이스를 만들 수 있습니다. 또한 알림, 안내 메시지, 네비게이션 등에서도 유용하게 활용됩니다.
기존에는 미리 녹음된 오디오 파일을 재생하거나 외부 TTS 서비스를 사용했다면, 이제는 브라우저만으로 동적으로 생성된 텍스트를 즉시 음성으로 변환할 수 있습니다. 핵심 특징은 다양한 음성 옵션(성별, 언어, 억양), 속도와 피치 조절, 그리고 이벤트 기반 제어입니다.
이러한 특징들이 상황에 맞는 자연스러운 음성 응답을 만들 수 있게 해줍니다.
코드 예제
// Speech Synthesis 객체
const synth = window.speechSynthesis;
// 음성 응답 함수
function speak(text, options = {}) {
// 기존 음성 중단
synth.cancel();
const utterance = new SpeechSynthesisUtterance(text);
// 음성 설정
utterance.lang = options.lang || 'ko-KR';
utterance.rate = options.rate || 1.0; // 속도 (0.1 ~ 10)
utterance.pitch = options.pitch || 1.0; // 피치 (0 ~ 2)
utterance.volume = options.volume || 1.0; // 볼륨 (0 ~ 1)
// 한국어 음성 선택
const voices = synth.getVoices();
const koreanVoice = voices.find(voice => voice.lang.includes('ko'));
if (koreanVoice) utterance.voice = koreanVoice;
// 이벤트 핸들러
utterance.onstart = () => console.log('음성 시작');
utterance.onend = () => console.log('음성 종료');
utterance.onerror = (e) => console.error('음성 오류:', e);
// 음성 재생
synth.speak(utterance);
}
// 사용 예시
speak('날씨 정보를 확인 중입니다', { rate: 1.2, pitch: 1.1 });
설명
이것이 하는 일: 이 코드는 텍스트를 받아서 브라우저의 음성 합성 엔진을 통해 자연스러운 음성으로 변환하고 재생하는 완전한 TTS 시스템입니다. 첫 번째로, speechSynthesis 객체를 가져와 음성 합성 기능에 접근합니다.
speak 함수 시작 부분에서 synth.cancel()을 호출하는 것이 중요한데, 이는 이전에 재생 중이던 음성을 중단하여 새로운 음성이 겹치지 않도록 합니다. 만약 이전 음성이 끝나기를 기다린다면 사용자는 오래된 정보를 계속 듣게 될 수 있습니다.
두 번째로, SpeechSynthesisUtterance 객체를 생성하고 다양한 속성을 설정합니다. rate는 말하는 속도를 조절하는데, 1.0이 기본 속도이고 값이 클수록 빨라집니다.
긴급한 알림은 1.21.5 정도로 빠르게, 차분한 안내는 0.80.9로 느리게 설정할 수 있습니다. pitch는 목소리의 높낮이를 조절하여 다양한 감정이나 상황을 표현할 수 있습니다.
세 번째로, getVoices() 메서드로 사용 가능한 모든 음성 목록을 가져와서 한국어 음성을 찾아 설정합니다. 브라우저와 운영체제에 따라 제공되는 음성이 다르므로, 여러 개 중에서 가장 적합한 것을 선택하는 로직이 필요합니다.
이벤트 핸들러를 등록하여 음성 재생의 시작, 종료, 오류를 추적할 수 있으며, 이를 통해 UI 업데이트나 로깅을 수행할 수 있습니다. 여러분이 이 코드를 사용하면 어떤 텍스트든 즉시 음성으로 변환하여 사용자에게 들려줄 수 있습니다.
상황에 따라 속도와 피치를 조절하여 긴급 알림은 빠르고 높은 목소리로, 일반 안내는 차분하고 낮은 목소리로 전달하는 등 맥락에 맞는 음성 경험을 제공할 수 있습니다. 또한 음성 재생 중 사용자가 다른 명령을 내리면 즉시 중단하고 새로운 응답을 들려주는 반응성 좋은 인터페이스를 구현할 수 있습니다.
실전 팁
💡 synth.getVoices()는 비동기적으로 로드되므로, speechSynthesis.onvoiceschanged 이벤트를 사용하여 음성 목록이 준비된 후에 설정하세요.
💡 긴 텍스트를 음성으로 변환할 때는 문장 단위로 나누어 재생하면 사용자가 중간에 중단할 수 있어 UX가 향상됩니다.
💡 모바일에서는 음성 재생이 사용자 제스처 후에만 가능하므로, 첫 음성은 버튼 클릭 등의 이벤트 핸들러 내에서 실행하세요.
💡 utterance.rate를 사용자가 설정할 수 있게 하면 개인별 선호도에 맞춘 경험을 제공할 수 있습니다 (설정에 "음성 속도" 옵션 추가).
💡 백그라운드에서 음성이 재생 중일 때 synth.speaking 속성으로 상태를 확인하여 중복 재생을 방지하세요.
5. 대화 컨텍스트 관리 - 상태 유지하기
시작하며
여러분이 음성 비서에게 "서울 날씨 알려줘"라고 물어본 후, "내일은?"이라고 추가 질문을 했을 때 비서가 "무엇의 내일을 말씀하시는 건가요?"라고 되묻는다면 어떨까요? 매우 답답할 것입니다.
이런 문제는 대화 컨텍스트를 유지하지 못해서 발생합니다. 실제 사람과의 대화는 이전 대화 내용을 기억하고 이어가는 것이 자연스럽습니다.
음성 비서도 마찬가지로 이전 명령과 응답을 기억하여 문맥을 이해해야 합니다. 그렇지 않으면 매번 전체 문장을 다시 말해야 하는 불편함이 생깁니다.
바로 이럴 때 필요한 것이 Conversation Context Management입니다. 대화 히스토리와 현재 상태를 추적하여 자연스러운 연속 대화가 가능해집니다.
개요
간단히 말해서, 대화 컨텍스트 관리는 사용자와의 대화 내용, 이전 명령의 파라미터, 현재 상태 등을 저장하고 추적하여 문맥을 이해하는 시스템입니다. 이 시스템이 필요한 이유는 자연스러운 대화형 인터페이스를 만들기 위해서입니다.
예를 들어, "타이머 설정"이라고 말한 후 "아니야 취소"라고 하면 방금 설정한 타이머를 취소할 수 있어야 하고, "음악 틀어줘" 후 "볼륨 높여줘"라고 하면 현재 재생 중인 음악의 볼륨을 조절해야 합니다. 기존에는 각 명령을 독립적으로 처리했다면, 이제는 세션 기반으로 상태를 유지하여 여러 명령이 연속적으로 이어지는 대화를 지원할 수 있습니다.
핵심 특징은 대화 히스토리 저장, 엔티티 추적, 그리고 상태 기반 응답입니다. 이러한 특징들이 사용자가 짧은 명령으로도 이전 문맥을 활용하여 소통할 수 있게 해줍니다.
코드 예제
// 대화 컨텍스트 관리 클래스
class ConversationContext {
constructor() {
this.history = []; // 대화 히스토리
this.entities = {}; // 현재 추적 중인 엔티티
this.lastIntent = null; // 마지막 의도
this.state = {}; // 현재 상태
}
// 새로운 명령 추가
addTurn(userInput, intent, entities, response) {
this.history.push({
timestamp: Date.now(),
userInput,
intent,
entities,
response
});
// 엔티티 업데이트 (새 값이 있으면 덮어쓰기)
this.entities = { ...this.entities, ...entities };
this.lastIntent = intent;
// 히스토리 크기 제한 (메모리 관리)
if (this.history.length > 10) {
this.history.shift();
}
}
// 이전 엔티티 가져오기
getEntity(entityType) {
return this.entities[entityType];
}
// 컨텍스트 초기화
reset() {
this.history = [];
this.entities = {};
this.lastIntent = null;
this.state = {};
}
// 이전 명령과 유사한지 확인
isSimilarToLast(intent) {
return this.lastIntent === intent;
}
}
// 사용 예시
const context = new ConversationContext();
// 첫 번째 명령: "서울 날씨 알려줘"
context.addTurn(
"서울 날씨 알려줘",
"weather",
{ location: "서울" },
"서울은 현재 맑음, 기온 15도입니다"
);
// 두 번째 명령: "내일은?" - 이전 location 재사용
const location = context.getEntity('location'); // "서울"
context.addTurn(
"내일은?",
"weather",
{ location: location, time: "내일" },
"내일 서울은 흐림, 기온 12도입니다"
);
설명
이것이 하는 일: 이 코드는 사용자와의 대화 내용을 저장하고 추적하여 이전 명령의 정보를 재사용하고 문맥을 이해할 수 있는 대화 관리 시스템입니다. 첫 번째로, ConversationContext 클래스의 생성자에서 대화를 관리하는 데 필요한 여러 속성을 초기화합니다.
history는 전체 대화 내용을 시간순으로 저장하고, entities는 현재 대화에서 추출된 중요한 정보(위치, 시간, 숫자 등)를 보관합니다. lastIntent는 바로 이전 명령의 의도를 저장하여 연속된 명령인지 판단할 수 있게 합니다.
이렇게 분리하여 관리하면 각 정보를 독립적으로 업데이트하고 조회할 수 있습니다. 두 번째로, addTurn 메서드가 실행되면서 새로운 대화 턴을 기록합니다.
사용자 입력, 인식된 의도, 추출된 엔티티, 그리고 시스템의 응답을 모두 timestamp와 함께 저장합니다. 중요한 부분은 엔티티 업데이트 로직인데, 스프레드 연산자를 사용하여 기존 엔티티를 유지하면서 새로운 엔티티만 추가하거나 덮어씁니다.
이렇게 하면 "서울 날씨"라고 말한 후 "내일은?"이라고 물어봤을 때 location은 "서울"을 유지하면서 time만 "내일"로 추가할 수 있습니다. 세 번째로, getEntity 메서드로 이전 대화에서 추출된 엔티티를 조회할 수 있습니다.
예시에서 보듯이 두 번째 명령 "내일은?"에는 위치 정보가 없지만, 첫 번째 명령의 "서울"을 재사용하여 "내일 서울 날씨"를 조회할 수 있습니다. 또한 히스토리가 너무 길어지면 메모리 문제가 발생할 수 있으므로, 10개 이상이 되면 가장 오래된 것을 제거하여 크기를 제한합니다.
여러분이 이 코드를 사용하면 사용자가 짧고 간결한 명령으로도 의사소통할 수 있는 자연스러운 대화형 비서를 만들 수 있습니다. "취소", "다시", "그거" 같은 대명사나 생략된 표현도 이전 문맥을 참조하여 처리할 수 있고, 연속된 질문에서 반복되는 파라미터를 매번 말하지 않아도 되어 사용자 경험이 크게 향상됩니다.
또한 대화 히스토리를 분석하여 사용자의 패턴을 파악하고 더 나은 응답을 제공하는 학습 기능도 추가할 수 있습니다.
실전 팁
💡 세션 타임아웃을 설정하여 일정 시간 이상 명령이 없으면 컨텍스트를 자동으로 리셋하세요. 예: 5분 이상 대화가 없으면 새로운 대화로 간주.
💡 민감한 정보(비밀번호, 카드 번호 등)는 히스토리에 저장하지 말고 즉시 삭제하여 보안을 강화하세요.
💡 사용자가 "처음부터 다시"나 "리셋" 같은 명령을 하면 컨텍스트를 초기화하여 새로운 대화를 시작할 수 있게 하세요.
💡 엔티티에 유효 기간을 설정하면 시간이 지나면 자동으로 제거되어 오래된 정보로 인한 오류를 방지할 수 있습니다.
💡 대화 히스토리를 로컬 스토리지에 저장하면 페이지를 새로고침해도 이전 대화를 이어갈 수 있습니다.
6. AI 모델 통합 - GPT API 연동하기
시작하며
여러분이 만든 음성 비서가 패턴 매칭으로 처리할 수 없는 복잡한 질문을 받으면 어떻게 해야 할까요? "양자역학이 뭐야?"나 "창의적인 생일 선물 아이디어 추천해줘" 같은 개방형 질문은 미리 정의된 패턴으로는 답변하기 어렵습니다.
이런 한계는 규칙 기반 시스템의 근본적인 문제입니다. 모든 가능한 질문에 대한 답변을 미리 준비할 수 없고, 사용자는 예상치 못한 다양한 방식으로 질문합니다.
제한된 명령만 처리할 수 있는 비서는 실용성이 크게 떨어집니다. 바로 이럴 때 필요한 것이 대규모 언어 모델(LLM) 통합입니다.
GPT 같은 AI 모델을 연동하면 거의 모든 종류의 질문에 지능적으로 답변할 수 있습니다.
개요
간단히 말해서, AI 모델 통합은 OpenAI GPT나 Claude 같은 대규모 언어 모델 API를 음성 비서에 연결하여 자연어 이해와 생성 능력을 극대화하는 것입니다. 이 통합이 필요한 이유는 명확합니다.
규칙 기반으로는 처리할 수 없는 복잡한 질문, 창의적인 요청, 추론이 필요한 작업 등을 처리하기 위해서입니다. 예를 들어, "내일 회의 준비를 위해 해야 할 일을 정리해줘"처럼 맥락 이해와 추론이 필요한 요청이나, "파이썬으로 간단한 웹 크롤러 만드는 법 설명해줘" 같은 기술적 질문에 답변할 수 있습니다.
기존에는 제한된 명령어만 처리하는 단순한 비서였다면, 이제는 거의 모든 질문에 답하고 복잡한 작업을 수행할 수 있는 진짜 AI 비서가 됩니다. 핵심 특징은 자연어 이해, 컨텍스트 기반 응답 생성, 그리고 대화 히스토리 활용입니다.
이러한 특징들이 사용자가 자유롭게 질문하고 자연스럽게 대화할 수 있는 진정한 음성 비서를 만들 수 있게 해줍니다.
코드 예제
// OpenAI API 통합
class AIAssistant {
constructor(apiKey) {
this.apiKey = apiKey;
this.apiUrl = 'https://api.openai.com/v1/chat/completions';
this.conversationHistory = [];
}
// AI 응답 생성
async generateResponse(userMessage, context = {}) {
// 대화 히스토리에 사용자 메시지 추가
this.conversationHistory.push({
role: 'user',
content: userMessage
});
// 시스템 프롬프트 구성
const systemPrompt = this.buildSystemPrompt(context);
try {
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: 'gpt-4',
messages: [
{ role: 'system', content: systemPrompt },
...this.conversationHistory
],
temperature: 0.7,
max_tokens: 150
})
});
const data = await response.json();
const assistantMessage = data.choices[0].message.content;
// 대화 히스토리에 AI 응답 추가
this.conversationHistory.push({
role: 'assistant',
content: assistantMessage
});
return assistantMessage;
} catch (error) {
console.error('AI 응답 생성 오류:', error);
return '죄송합니다. 응답을 생성하는 중 오류가 발생했습니다.';
}
}
// 시스템 프롬프트 구성
buildSystemPrompt(context) {
return `당신은 친절한 음성 비서입니다.
사용자의 질문에 간결하고 명확하게 답변하세요.
현재 시간: ${new Date().toLocaleString('ko-KR')}
사용자 위치: ${context.location || '알 수 없음'}
간단히 2-3 문장으로 답변하세요.`;
}
// 대화 히스토리 초기화
resetConversation() {
this.conversationHistory = [];
}
}
// 사용 예시
const assistant = new AIAssistant('your-api-key');
const response = await assistant.generateResponse(
"오늘 날씨에 맞는 옷차림 추천해줘",
{ location: "서울" }
);
설명
이것이 하는 일: 이 코드는 OpenAI GPT API와 연동하여 사용자의 자연어 질문을 이해하고 맥락에 맞는 지능적인 응답을 생성하는 AI 비서 시스템입니다. 첫 번째로, AIAssistant 클래스 생성자에서 API 키와 엔드포인트 URL을 설정하고, 대화 히스토리를 저장할 배열을 초기화합니다.
대화 히스토리를 유지하는 것이 중요한데, GPT 모델은 이전 대화 내용을 참조하여 문맥에 맞는 응답을 생성하기 때문입니다. 예를 들어 "그거 더 자세히 설명해줘"라고 하면 이전 대화를 보고 '그거'가 무엇인지 파악할 수 있습니다.
두 번째로, generateResponse 메서드가 실행되면서 사용자 메시지를 히스토리에 추가하고 API 요청을 준비합니다. buildSystemPrompt 메서드로 시스템 프롬프트를 구성하는데, 여기에는 비서의 역할, 현재 시간, 사용자 위치 등의 컨텍스트 정보를 포함시킵니다.
이렇게 하면 AI가 "지금 몇 시야?"라는 질문에 실제 현재 시간으로 답변할 수 있고, 위치에 맞는 정보를 제공할 수 있습니다. 세 번째로, fetch API로 OpenAI 엔드포인트에 POST 요청을 보냅니다.
temperature 파라미터는 응답의 창의성을 조절하는데, 0.7은 적당히 창의적이면서도 일관성 있는 값입니다. max_tokens는 응답 길이를 제한하여 음성으로 읽기에 적절한 길이로 유지합니다.
너무 긴 응답은 음성으로 들으면 지루하기 때문입니다. 응답을 받으면 대화 히스토리에 추가하여 다음 대화에서 참조할 수 있게 합니다.
여러분이 이 코드를 사용하면 거의 모든 종류의 질문에 답변할 수 있는 강력한 음성 비서를 만들 수 있습니다. 날씨, 뉴스, 일반 상식뿐만 아니라 복잡한 추론, 창의적인 아이디어 생성, 코드 작성 도움 등 다양한 작업을 수행할 수 있습니다.
대화 히스토리를 활용하여 연속적인 대화가 가능하고, 컨텍스트 정보를 전달하여 사용자의 상황에 맞는 개인화된 응답을 제공할 수 있습니다.
실전 팁
💡 API 호출 비용을 절약하려면 간단한 명령(타이머, 날씨 등)은 로컬 패턴 매칭으로 처리하고, 복잡한 질문만 AI에게 전달하세요.
💡 응답 시간이 길어질 수 있으므로 "생각하는 중입니다..." 같은 음성 피드백을 먼저 재생하여 UX를 개선하세요.
💡 대화 히스토리가 너무 길어지면 토큰 비용이 증가하므로, 최근 5-10턴만 유지하고 오래된 것은 제거하세요.
💡 민감한 정보(개인정보, 비밀번호 등)는 API에 전송하지 않도록 필터링 로직을 추가하여 프라이버시를 보호하세요.
💡 네트워크 오류나 API 장애에 대비하여 재시도 로직과 폴백 응답을 준비하세요. 예: "죄송합니다. 지금은 답변할 수 없습니다."
7. 웨이크워드 감지 - "헤이 비서" 구현하기
시작하며
여러분이 스마트 스피커를 사용할 때 매번 버튼을 눌러야 한다면 얼마나 불편할까요? "알렉사", "OK 구글"처럼 특정 단어를 말하면 자동으로 활성화되는 것이 훨씬 자연스럽습니다.
이런 웨이크워드(깨우기 단어) 기능이 없으면 음성 비서는 핸즈프리가 아닙니다. 요리 중이거나 운전 중처럼 손을 사용할 수 없는 상황에서는 버튼을 누를 수 없으므로 음성 비서의 가치가 크게 떨어집니다.
또한 항상 음성 인식이 켜져 있으면 프라이버시 문제와 불필요한 처리로 인한 성능 저하가 발생합니다. 바로 이럴 때 필요한 것이 Wake Word Detection입니다.
특정 키워드를 감지하여 음성 비서를 활성화하고, 나머지 시간에는 대기 상태를 유지할 수 있습니다.
개요
간단히 말해서, 웨이크워드 감지는 연속적으로 음성을 모니터링하다가 특정 단어("헤이 비서", "OK 비서" 등)를 인식하면 메인 음성 인식을 활성화하는 시스템입니다. 이 기능이 필요한 이유는 진정한 핸즈프리 경험을 제공하고, 프라이버시를 보호하며, 배터리와 리소스를 절약하기 위해서입니다.
예를 들어, 스마트 홈에서 집 안 어디서든 "헤이 비서, 거실 불 켜줘"라고 말하면 즉시 반응하는 편리한 경험을 제공할 수 있습니다. 기존에는 물리적 버튼이나 화면 터치가 필요했다면, 이제는 음성만으로 완전히 제어할 수 있는 자연스러운 인터페이스를 만들 수 있습니다.
핵심 특징은 저전력 연속 모니터링, 특정 단어 패턴 인식, 그리고 오인식 방지입니다. 이러한 특징들이 항상 대기하면서도 효율적이고 정확한 웨이크워드 감지를 가능하게 합니다.
코드 예제
// 웨이크워드 감지 시스템
class WakeWordDetector {
constructor(wakeWords = ['헤이 비서', '오케이 비서']) {
this.wakeWords = wakeWords;
this.isListening = false;
this.recognition = null;
this.onWakeWordDetected = null;
}
// 웨이크워드 모니터링 시작
startMonitoring() {
const SpeechRecognition = window.SpeechRecognition ||
window.webkitSpeechRecognition;
this.recognition = new SpeechRecognition();
// 연속 인식 설정
this.recognition.continuous = true;
this.recognition.interimResults = false;
this.recognition.lang = 'ko-KR';
this.recognition.onresult = (event) => {
const lastResult = event.results[event.results.length - 1];
const transcript = lastResult[0].transcript.toLowerCase();
console.log('감지된 음성:', transcript);
// 웨이크워드 확인
const wakeWordDetected = this.wakeWords.some(word =>
transcript.includes(word.toLowerCase())
);
if (wakeWordDetected && !this.isListening) {
console.log('웨이크워드 감지!');
this.isListening = true;
// 웨이크워드 감지 콜백 실행
if (this.onWakeWordDetected) {
this.onWakeWordDetected();
}
// 일정 시간 후 대기 상태로 복귀
setTimeout(() => {
this.isListening = false;
}, 10000); // 10초 후 비활성화
}
};
this.recognition.onerror = (event) => {
console.error('웨이크워드 감지 오류:', event.error);
// 오류 발생 시 재시작
setTimeout(() => this.recognition.start(), 1000);
};
this.recognition.onend = () => {
// 중단되면 자동으로 재시작
if (!this.isListening) {
this.recognition.start();
}
};
this.recognition.start();
console.log('웨이크워드 모니터링 시작...');
}
// 모니터링 중단
stopMonitoring() {
if (this.recognition) {
this.recognition.stop();
}
}
}
// 사용 예시
const detector = new WakeWordDetector(['헤이 비서']);
detector.onWakeWordDetected = () => {
console.log('음성 비서 활성화됨!');
speak('네, 무엇을 도와드릴까요?');
// 메인 음성 인식 시작
};
detector.startMonitoring();
설명
이것이 하는 일: 이 코드는 백그라운드에서 계속 음성을 모니터링하다가 사용자가 웨이크워드("헤이 비서" 등)를 말하면 자동으로 음성 비서를 활성화하는 시스템입니다. 첫 번째로, WakeWordDetector 클래스 생성자에서 감지할 웨이크워드 목록을 설정합니다.
여러 개의 웨이크워드를 지원하여 사용자가 선호하는 단어를 선택할 수 있게 합니다. isListening 플래그는 현재 비서가 활성화되어 있는지 추적하여 중복 활성화를 방지합니다.
onWakeWordDetected 콜백 함수를 외부에서 설정할 수 있게 하여 웨이크워드 감지 시 원하는 동작을 실행할 수 있습니다. 두 번째로, startMonitoring 메서드에서 SpeechRecognition을 설정하고 시작합니다.
continuous: true로 설정하여 인식이 중단되지 않고 계속 실행되게 합니다. onresult 이벤트에서는 인식된 텍스트에 웨이크워드가 포함되어 있는지 확인합니다.
some 메서드를 사용하여 여러 웨이크워드 중 하나라도 매칭되면 true를 반환합니다. 이미 활성화 상태가 아닐 때만 활성화하여 반복적인 웨이크워드 감지를 방지합니다.
세 번째로, 웨이크워드가 감지되면 isListening을 true로 설정하고 콜백 함수를 실행합니다. 콜백에서는 음성 피드백("네, 무엇을 도와드릴까요?")을 재생하고 메인 음성 인식을 시작할 수 있습니다.
중요한 것은 일정 시간(10초) 후 자동으로 대기 상태로 돌아가는 것인데, 이렇게 하지 않으면 비서가 계속 활성화되어 있어서 일상 대화를 모두 명령으로 인식할 수 있습니다. onerror와 onend 핸들러로 오류나 중단 시 자동으로 재시작하여 항상 모니터링 상태를 유지합니다.
여러분이 이 코드를 사용하면 스마트 스피커처럼 버튼 없이 음성만으로 제어할 수 있는 진정한 핸즈프리 음성 비서를 만들 수 있습니다. 집안 어디서든 웨이크워드만 말하면 즉시 반응하고, 명령이 끝나면 자동으로 대기 상태로 돌아가 프라이버시를 보호합니다.
또한 오류나 중단이 발생해도 자동으로 복구되어 안정적인 서비스를 제공할 수 있습니다.
실전 팁
💡 웨이크워드는 2-3음절 이상의 독특한 단어를 사용하세요. "안녕", "네" 같은 일상 단어는 오인식이 많습니다.
💡 웨이크워드 감지 후 짧은 알림음(beep)을 재생하면 사용자가 활성화를 명확히 인지할 수 있습니다.
💡 배터리 절약을 위해 일정 시간(예: 1분) 동안 명령이 없으면 모니터링을 일시 중지하고 버튼 터치로 재활성화하는 옵션을 제공하세요.
💡 신뢰도 임계값을 설정하여 낮은 신뢰도의 인식 결과는 무시하면 오인식을 크게 줄일 수 있습니다.
💡 사용자 설정에서 웨이크워드를 직접 설정할 수 있게 하면 개인화된 경험을 제공할 수 있습니다.
8. 에러 처리와 폴백 - 안정성 확보하기
시작하며
여러분의 음성 비서가 완벽하게 작동하다가 갑자기 마이크 권한이 거부되거나 네트워크가 끊기면 어떻게 될까요? 아무 반응도 없거나 에러 메시지만 표시된다면 사용자는 크게 실망할 것입니다.
이런 오류 상황은 실제 환경에서 자주 발생합니다. 마이크 접근 권한 거부, 네트워크 오류, API 호출 실패, 브라우저 비호환성 등 다양한 예외 상황이 있습니다.
이러한 상황을 제대로 처리하지 못하면 사용자는 비서가 고장났다고 생각하고 사용을 포기하게 됩니다. 바로 이럴 때 필요한 것이 Error Handling과 Fallback Strategy입니다.
모든 가능한 오류를 예측하고 적절히 처리하여 안정적인 서비스를 제공할 수 있습니다.
개요
간단히 말해서, 에러 처리는 예외 상황을 감지하고 사용자에게 명확한 피드백을 제공하며, 가능하면 대안적인 방법으로 서비스를 계속 제공하는 시스템입니다. 이 시스템이 필요한 이유는 실제 환경의 불확실성을 관리하고 사용자 경험을 보호하기 위해서입니다.
예를 들어, 음성 인식이 실패하면 텍스트 입력 옵션을 제공하거나, AI API가 응답하지 않으면 로컬 패턴 매칭으로 대체하거나, 마이크가 없으면 텍스트 기반 모드로 전환할 수 있습니다. 기존에는 오류 발생 시 단순히 중단되었다면, 이제는 우아하게 저하(graceful degradation)하여 제한적이나마 서비스를 계속 제공할 수 있습니다.
핵심 특징은 포괄적인 에러 감지, 사용자 친화적 피드백, 그리고 폴백 전략입니다. 이러한 특징들이 어떤 상황에서도 최소한의 기능을 제공하는 안정적인 음성 비서를 만들 수 있게 해줍니다.
코드 예제
// 에러 처리 및 폴백 시스템
class ErrorHandler {
constructor() {
this.errorLog = [];
this.fallbackMode = false;
}
// 음성 인식 에러 처리
handleSpeechRecognitionError(error) {
const errorMessages = {
'no-speech': '음성이 감지되지 않았습니다. 다시 시도해주세요.',
'audio-capture': '마이크에 접근할 수 없습니다. 권한을 확인해주세요.',
'not-allowed': '마이크 사용 권한이 거부되었습니다.',
'network': '네트워크 연결을 확인해주세요.',
'aborted': '음성 인식이 중단되었습니다.',
'service-not-allowed': '음성 인식 서비스를 사용할 수 없습니다.'
};
const message = errorMessages[error.error] ||
'음성 인식 중 오류가 발생했습니다.';
this.logError('SpeechRecognition', error.error, message);
this.notifyUser(message);
// 폴백: 텍스트 입력 모드 제안
if (error.error === 'not-allowed' || error.error === 'audio-capture') {
this.enableFallbackMode('text-input');
}
return message;
}
// API 호출 에러 처리
async handleAPIError(error, retryFn, maxRetries = 3) {
this.logError('API', error.name, error.message);
// 재시도 로직
for (let i = 0; i < maxRetries; i++) {
console.log(`재시도 ${i + 1}/${maxRetries}...`);
await this.delay(1000 * (i + 1)); // 지수 백오프
try {
return await retryFn();
} catch (retryError) {
if (i === maxRetries - 1) {
// 최대 재시도 실패
this.notifyUser('서비스에 연결할 수 없습니다. 나중에 다시 시도해주세요.');
this.enableFallbackMode('offline');
throw retryError;
}
}
}
}
// 사용자에게 알림
notifyUser(message) {
console.log('사용자 알림:', message);
speak(message); // 음성으로 알림
// UI에도 표시 (선택사항)
}
// 폴백 모드 활성화
enableFallbackMode(mode) {
this.fallbackMode = mode;
console.log(`폴백 모드 활성화: ${mode}`);
if (mode === 'text-input') {
this.notifyUser('텍스트 입력 모드로 전환합니다.');
} else if (mode === 'offline') {
this.notifyUser('오프라인 모드로 전환합니다. 제한된 기능만 사용 가능합니다.');
}
}
// 에러 로깅
logError(category, type, message) {
const errorEntry = {
timestamp: new Date().toISOString(),
category,
type,
message
};
this.errorLog.push(errorEntry);
console.error('에러 로그:', errorEntry);
}
// 지연 함수 (재시도용)
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 사용 예시
const errorHandler = new ErrorHandler();
recognition.onerror = (event) => {
errorHandler.handleSpeechRecognitionError(event);
};
// API 호출 시
try {
const response = await errorHandler.handleAPIError(
new Error('Network error'),
() => fetch('https://api.example.com/data')
);
} catch (error) {
console.log('최종 실패, 폴백 모드 사용');
}
설명
이것이 하는 일: 이 코드는 음성 인식, API 호출 등에서 발생할 수 있는 모든 오류를 체계적으로 처리하고, 재시도 및 대안 방법을 제공하여 서비스의 안정성을 확보하는 시스템입니다. 첫 번째로, ErrorHandler 클래스에서 에러 로그를 관리하고 폴백 모드 상태를 추적합니다.
handleSpeechRecognitionError 메서드는 음성 인식에서 발생할 수 있는 다양한 에러 타입별로 사용자 친화적인 메시지를 매핑합니다. 'no-speech'는 단순히 다시 시도하면 되지만, 'not-allowed'는 권한 문제이므로 사용자에게 설정을 변경하도록 안내해야 합니다.
각 에러마다 적절한 메시지와 대응 방법을 제공하는 것이 중요합니다. 두 번째로, handleAPIError 메서드는 네트워크 오류나 API 장애 시 자동으로 재시도합니다.
지수 백오프(exponential backoff) 전략을 사용하여 첫 번째 재시도는 1초 후, 두 번째는 2초 후, 세 번째는 3초 후에 실행합니다. 이렇게 하는 이유는 서버 부하를 줄이고, 일시적인 네트워크 문제가 해결될 시간을 주기 위해서입니다.
최대 재시도 횟수만큼 시도한 후에도 실패하면 폴백 모드로 전환하여 제한적이나마 서비스를 계속 제공합니다. 세 번째로, enableFallbackMode 메서드로 다양한 폴백 전략을 활성화합니다.
마이크 접근이 불가능하면 텍스트 입력 모드로 전환하고, API가 응답하지 않으면 오프라인 모드로 전환하여 로컬 패턴 매칭만 사용합니다. 사용자에게는 음성과 UI로 상황을 명확히 알려서 혼란을 방지합니다.
모든 에러는 logError 메서드로 기록하여 나중에 분석하고 개선할 수 있습니다. 여러분이 이 코드를 사용하면 예상치 못한 오류 상황에서도 사용자에게 명확한 피드백을 제공하고 대안을 제시할 수 있습니다.
일시적인 네트워크 문제는 자동으로 재시도하여 해결하고, 권한 문제는 사용자에게 명확히 안내하며, 심각한 오류는 폴백 모드로 전환하여 최소한의 기능이라도 제공합니다. 이렇게 하면 사용자는 비서가 항상 신뢰할 수 있다고 느끼고, 오류 로그를 분석하여 지속적으로 서비스를 개선할 수 있습니다.
실전 팁
💡 에러 메시지는 기술적인 용어보다 사용자가 이해하기 쉬운 일상 언어로 작성하세요. "audio-capture failed" 대신 "마이크를 찾을 수 없습니다".
💡 재시도 전에 사용자에게 "다시 시도하는 중입니다"라고 알려주면 기다림의 이유를 알고 안심할 수 있습니다.
💡 에러 로그를 서버로 전송하여 실제 사용자들이 겪는 문제를 모니터링하고 우선순위를 정하여 개선하세요.
💡 폴백 모드에서도 핵심 기능은 제공할 수 있도록 중요한 명령은 로컬에서 처리할 수 있게 준비하세요.
💡 브라우저 호환성 체크를 추가하여 지원하지 않는 브라우저에서는 처음부터 텍스트 모드를 권장하세요.
9. 다국어 지원 - 글로벌 음성 비서 만들기
시작하며
여러분의 음성 비서가 한국어만 지원한다면 글로벌 시장에서 얼마나 경쟁력이 있을까요? 영어, 중국어, 일본어 사용자는 사용할 수 없어서 시장이 크게 제한됩니다.
다국어 지원 없이는 글로벌 서비스를 제공할 수 없습니다. 같은 기능이라도 사용자의 언어로 소통하지 못하면 사용성이 크게 떨어집니다.
또한 다국어를 구현할 때 단순 번역만으로는 부족하고, 언어별 문화적 차이, 표현 방식, 음성 인식 정확도 등을 모두 고려해야 합니다. 바로 이럴 때 필요한 것이 Multilingual Support입니다.
언어별 음성 인식, 응답 생성, 그리고 로컬라이제이션을 체계적으로 관리할 수 있습니다.
개요
간단히 말해서, 다국어 지원은 여러 언어로 음성을 인식하고, 각 언어에 맞는 응답을 생성하며, 문화적으로 적절한 표현을 사용하는 시스템입니다. 이 기능이 필요한 이유는 글로벌 사용자 기반을 확보하고 각 지역 사용자에게 최적화된 경험을 제공하기 위해서입니다.
예를 들어, 미국 사용자는 화씨 온도와 마일을 선호하고, 한국 사용자는 섭씨와 킬로미터를 선호하므로, 같은 날씨 정보도 다르게 표현해야 합니다. 기존에는 한 언어로만 서비스했다면, 이제는 여러 언어를 자동으로 감지하고 적절히 응답하는 진정한 글로벌 음성 비서를 만들 수 있습니다.
핵심 특징은 언어 자동 감지, 언어별 리소스 관리, 그리고 문화적 로컬라이제이션입니다. 이러한 특징들이 전 세계 어디서든 자연스럽게 사용할 수 있는 음성 비서를 만들 수 있게 해줍니다.
코드 예제
// 다국어 지원 시스템
class MultilingualAssistant {
constructor() {
this.currentLang = 'ko';
this.supportedLangs = ['ko', 'en', 'ja', 'zh'];
this.recognition = null;
this.responses = this.loadLanguageResources();
}
// 언어별 리소스 로드
loadLanguageResources() {
return {
ko: {
greeting: '안녕하세요. 무엇을 도와드릴까요?',
farewell: '감사합니다. 좋은 하루 되세요.',
error: '죄송합니다. 다시 한번 말씀해주세요.',
units: { temp: '°C', distance: 'km' }
},
en: {
greeting: 'Hello. How can I help you?',
farewell: 'Thank you. Have a great day.',
error: 'Sorry, could you repeat that?',
units: { temp: '°F', distance: 'miles' }
},
ja: {
greeting: 'こんにちは。何をお手伝いしましょうか?',
farewell: 'ありがとうございます。良い一日を。',
error: 'すみません。もう一度お願いします。',
units: { temp: '°C', distance: 'km' }
},
zh: {
greeting: '你好。我能帮你什么?',
farewell: '谢谢。祝你有美好的一天。',
error: '对不起,请再说一遍。',
units: { temp: '°C', distance: '公里' }
}
};
}
// 언어 설정
setLanguage(lang) {
if (!this.supportedLangs.includes(lang)) {
console.warn(`지원하지 않는 언어: ${lang}`);
return false;
}
this.currentLang = lang;
this.updateRecognitionLanguage(lang);
return true;
}
// 음성 인식 언어 업데이트
updateRecognitionLanguage(lang) {
const langCodes = {
ko: 'ko-KR',
en: 'en-US',
ja: 'ja-JP',
zh: 'zh-CN'
};
if (this.recognition) {
this.recognition.lang = langCodes[lang];
}
}
// 응답 가져오기
getResponse(key, params = {}) {
let response = this.responses[this.currentLang][key];
// 파라미터 치환
Object.keys(params).forEach(param => {
response = response.replace(`{${param}}`, params[param]);
});
return response;
}
// 언어 자동 감지
detectLanguage(text) {
// 간단한 언어 감지 (실제로는 더 정교한 알고리즘 필요)
if (/[\uac00-\ud7af]/.test(text)) return 'ko'; // 한글
if (/[\u3040-\u309f\u30a0-\u30ff]/.test(text)) return 'ja'; // 일본어
if (/[\u4e00-\u9fff]/.test(text)) return 'zh'; // 중국어
return 'en'; // 기본값
}
// 온도 변환 (로컬라이제이션)
formatTemperature(celsius) {
if (this.currentLang === 'en') {
const fahrenheit = (celsius * 9/5) + 32;
return `${fahrenheit.toFixed(1)}${this.responses.en.units.temp}`;
}
return `${celsius}${this.responses[this.currentLang].units.temp}`;
}
}
// 사용 예시
const assistant = new MultilingualAssistant();
// 영어로 전환
assistant.setLanguage('en');
console.log(assistant.getResponse('greeting'));
// "Hello. How can I help you?"
// 온도 포맷팅
console.log(assistant.formatTemperature(20));
// "68.0°F" (영어) 또는 "20°C" (한국어)
설명
이것이 하는 일: 이 코드는 여러 언어를 지원하고, 각 언어에 맞는 음성 인식과 응답을 제공하며, 문화적 차이를 반영한 포맷팅을 수행하는 다국어 음성 비서 시스템입니다. 첫 번째로, MultilingualAssistant 클래스에서 지원하는 언어 목록과 현재 언어를 관리합니다.
loadLanguageResources 메서드는 각 언어별로 자주 사용하는 응답 문구와 단위 시스템을 정의합니다. 이렇게 리소스를 중앙에서 관리하면 번역을 추가하거나 수정하기가 매우 쉽습니다.
예를 들어 새로운 응답을 추가할 때 각 언어별 객체에만 추가하면 되고, 코드 로직은 변경할 필요가 없습니다. 두 번째로, setLanguage 메서드로 현재 사용 언어를 변경합니다.
지원하지 않는 언어가 요청되면 경고를 출력하고 false를 반환하여 호출자가 적절히 대응할 수 있게 합니다. updateRecognitionLanguage 메서드는 음성 인식 엔진의 언어 설정을 업데이트하는데, 각 언어마다 로케일 코드(ko-KR, en-US 등)가 다르므로 매핑 테이블을 사용합니다.
이렇게 하면 'en'만 전달해도 자동으로 'en-US'로 변환되어 편리합니다. 세 번째로, getResponse 메서드는 키와 파라미터를 받아 현재 언어에 맞는 응답 문자열을 반환합니다.
파라미터 치환 기능을 통해 "안녕하세요, {name}님"처럼 동적인 응답을 생성할 수 있습니다. formatTemperature 메서드는 문화적 로컬라이제이션의 좋은 예인데, 같은 20도라도 영어권에서는 68°F로 변환하여 표시합니다.
detectLanguage 메서드는 정규표현식으로 유니코드 범위를 확인하여 언어를 자동 감지하므로, 사용자가 언어를 명시적으로 설정하지 않아도 자동으로 적절한 언어로 응답할 수 있습니다. 여러분이 이 코드를 사용하면 글로벌 시장을 타겟으로 하는 음성 비서를 쉽게 만들 수 있습니다.
새로운 언어를 추가할 때도 리소스 객체에 번역만 추가하면 되므로 확장이 매우 간단하고, 각 언어별 문화적 차이(단위, 날짜 형식, 표현 방식 등)를 반영하여 현지 사용자에게 자연스러운 경험을 제공할 수 있습니다. 언어 자동 감지 기능으로 사용자가 언어를 전환하지 않아도 알아서 인식하여 편의성을 높일 수 있습니다.
실전 팁
💡 Google Translate API나 i18next 같은 라이브러리를 사용하면 더 체계적이고 전문적인 다국어 관리가 가능합니다.
💡 언어별로 날짜 형식도 다르므로 moment.js나 day.js의 로케일 기능을 활용하여 "2025-01-15"를 "January 15, 2025" (영어) 또는 "2025년 1월 15일" (한국어)로 표시하세요.
💡 음성 합성(TTS)도 언어별로 다른 목소리를 사용하면 더 자연스럽습니다. getVoices()로 해당 언어의 네이티브 음성을 선택하세요.
💡 RTL(오른쪽에서 왼쪽) 언어(아랍어, 히브리어)를 지원한다면 UI 레이아웃도 반전해야 하므로 CSS direction 속성을 동적으로 변경하세요.
💡 사용자의 브라우저 언어 설정을 읽어(navigator.language) 초기 언어를 자동으로 설정하면 별도의 언어 선택 없이 바로 사용할 수 있습니다.
10. 음성 명령 큐잉 - 연속 명령 처리하기
시작하며
여러분이 음성 비서에게 "알람 맞춰주고, 날씨 알려주고, 음악 틀어줘"처럼 여러 명령을 한 번에 말하면 어떻게 처리해야 할까요? 첫 번째 명령만 처리하고 나머지를 무시하면 사용자는 불편함을 느낄 것입니다.
이런 연속 명령 처리는 자연스러운 대화형 인터페이스의 핵심입니다. 사람은 한 번에 여러 가지를 요청하는 경우가 많은데, 비서가 하나씩만 처리한다면 매번 기다렸다가 다시 말해야 하는 번거로움이 생깁니다.
또한 명령들이 순서대로 실행되지 않으면 논리적 오류가 발생할 수 있습니다. 바로 이럴 때 필요한 것이 Command Queuing System입니다.
여러 명령을 파싱하고, 큐에 저장하며, 순차적으로 실행할 수 있습니다.
개요
간단히 말해서, 명령 큐잉은 하나의 음성 입력에서 여러 개의 명령을 추출하고, 순서를 유지하며 하나씩 실행하는 시스템입니다. 이 시스템이 필요한 이유는 효율적이고 자연스러운 사용자 경험을 제공하기 위해서입니다.
예를 들어, "내일 회의 일정 추가하고 오늘 할 일 목록 보여줘"처럼 관련된 여러 작업을 한 번에 요청할 수 있으면 훨씬 편리합니다. 또한 일부 명령이 실패해도 나머지는 계속 실행할 수 있어 견고성이 향상됩니다.
기존에는 한 번에 하나의 명령만 처리했다면, 이제는 여러 명령을 지능적으로 분리하고 관리하여 복잡한 워크플로우를 지원할 수 있습니다. 핵심 특징은 명령 분리 및 파싱, 우선순위 큐 관리, 그리고 비동기 실행입니다.
이러한 특징들이 사용자가 자연스럽게 여러 작업을 요청하고 효율적으로 처리할 수 있게 해줍니다.
코드 예제
// 명령 큐잉 시스템
class CommandQueue {
constructor() {
this.queue = [];
this.isProcessing = false;
this.separators = ['그리고', '하고', ',', '다음에', '그다음'];
}
// 문장을 여러 명령으로 분리
parseMultipleCommands(text) {
const commands = [];
let currentCommand = text;
// 구분자로 분리
this.separators.forEach(separator => {
const parts = currentCommand.split(separator);
if (parts.length > 1) {
commands.push(...parts.map(p => p.trim()).filter(p => p));
return;
}
});
// 구분자가 없으면 단일 명령
if (commands.length === 0) {
commands.push(text.trim());
}
return commands;
}
// 명령 큐에 추가
async enqueue(text) {
const commands = this.parseMultipleCommands(text);
console.log(`${commands.length}개의 명령 감지:`, commands);
// 각 명령을 큐에 추가
for (const cmd of commands) {
const intent = recognizeIntent(cmd); // 이전에 만든 의도 인식 함수
const params = extractParameters(cmd, ['duration', 'location', 'time']);
this.queue.push({
id: Date.now() + Math.random(),
text: cmd,
intent: intent.intent,
params: params,
status: 'pending'
});
}
// 큐 처리 시작
if (!this.isProcessing) {
await this.processQueue();
}
}
// 큐 처리
async processQueue() {
if (this.queue.length === 0) {
this.isProcessing = false;
speak('모든 작업을 완료했습니다.');
return;
}
this.isProcessing = true;
const command = this.queue.shift(); // 첫 번째 명령 가져오기
console.log('실행 중:', command.text);
command.status = 'processing';
try {
// 명령 실행
await this.executeCommand(command);
command.status = 'completed';
} catch (error) {
console.error('명령 실행 오류:', error);
command.status = 'failed';
speak(`${command.text} 실행 중 오류가 발생했습니다.`);
}
// 다음 명령 처리 (재귀)
await this.processQueue();
}
// 명령 실행
async executeCommand(command) {
// 의도별 액션 실행
switch (command.intent) {
case 'weather':
speak(`날씨 정보를 확인합니다.`);
// 실제 날씨 API 호출
break;
case 'timer':
const duration = command.params.duration;
speak(`${duration.value}${duration.unit} 타이머를 설정합니다.`);
// 실제 타이머 설정
break;
case 'music':
speak('음악을 재생합니다.');
// 실제 음악 재생
break;
default:
speak('알 수 없는 명령입니다.');
}
// 명령 처리 시뮬레이션 (실제로는 비동기 작업)
await new Promise(resolve => setTimeout(resolve, 1000));
}
// 큐 상태 확인
getStatus() {
return {
pending: this.queue.length,
isProcessing: this.isProcessing,
queue: this.queue
};
}
}
// 사용 예시
const commandQueue = new CommandQueue();
// 여러 명령 한 번에 처리
await commandQueue.enqueue(
"서울 날씨 알려주고 30분 타이머 맞춰줘 그리고 음악 틀어줘"
);
// 3개의 명령이 순차적으로 실행됨
설명
이것이 하는 일: 이 코드는 사용자가 한 번에 말한 여러 개의 명령을 자동으로 분리하고, 큐에 저장하여 순서대로 하나씩 실행하는 지능형 명령 관리 시스템입니다. 첫 번째로, CommandQueue 클래스에서 명령 큐와 처리 상태를 관리합니다.
parseMultipleCommands 메서드는 하나의 긴 문장을 여러 명령으로 분리하는 핵심 기능입니다. "그리고", "하고", "다음에" 같은 자연어 구분자를 사용하여 문장을 나눕니다.
예를 들어 "날씨 알려주고 타이머 맞춰줘"는 "날씨 알려주", "타이머 맞춰줘" 두 개의 명령으로 분리됩니다. 한국어의 자연스러운 표현을 모두 지원하기 위해 여러 구분자를 정의했습니다.
두 번째로, enqueue 메서드가 실행되면서 분리된 각 명령에 대해 의도 인식과 파라미터 추출을 수행합니다. 각 명령은 고유 ID, 원본 텍스트, 인식된 의도, 추출된 파라미터, 그리고 상태('pending', 'processing', 'completed', 'failed')를 포함하는 객체로 변환되어 큐에 추가됩니다.
이렇게 구조화된 데이터로 관리하면 나중에 큐 상태를 조회하거나 실패한 명령을 재시도하는 등의 고급 기능을 쉽게 구현할 수 있습니다. 세 번째로, processQueue 메서드가 재귀적으로 실행되면서 큐의 명령들을 하나씩 처리합니다.
shift() 메서드로 큐의 첫 번째 명령을 가져와 executeCommand로 실행하고, 완료되면 다음 명령을 처리합니다. 비동기 처리를 사용하여 각 명령이 완료될 때까지 기다린 후 다음 명령을 실행하므로, 명령 간 의존성이 있어도 문제없이 처리됩니다.
예를 들어 "알람 설정하고 알람 목록 보여줘"처럼 첫 번째 명령의 결과가 두 번째 명령에 영향을 주는 경우에도 정확히 동작합니다. 여러분이 이 코드를 사용하면 사용자가 자연스럽게 여러 작업을 한 번에 요청할 수 있어 사용성이 크게 향상됩니다.
"회의 일정 추가하고 이메일 보내고 리마인더 설정해줘" 같은 복잡한 워크플로우도 한 번의 음성 명령으로 처리할 수 있습니다. 또한 일부 명령이 실패해도 나머지 명령은 계속 실행되며, 큐 상태를 조회하여 진행 상황을 UI에 표시하거나 로그로 남길 수 있습니다.
실전 팁
💡 우선순위 기능을 추가하여 긴급한 명령("불 꺼줘")은 큐의 앞쪽으로 이동시켜 먼저 실행하세요.
💡 명령 간 의존성을 감지하여 순서를 자동으로 조정할 수 있습니다. 예: "음악 틀어줘 그리고 볼륨 높여줘"는 순서가 중요.
💡 사용자에게 현재 진행 상황을 음성으로 알려주면 좋습니다. "3개 중 1번째 작업을 실행하고 있습니다."
💡 큐에 저장된 명령을 로컬 스토리지에 백업하면 페이지 새로고침 후에도 이어서 실행할 수 있습니다.
💡 "취소", "중단" 명령을 인식하여 현재 큐를 비우고 모든 작업을 중단할 수 있는 기능을 제공하세요.