- Published on
- ·10 min read
react-native-tts 사용 시 발생한 경고(Warning) 및 에러(Error) 해결
들어가며
개발 중인 React Native 프로젝트에 TTS(Text To Speech) 기능을 추가하기로 하고 react-native-tts 라이브러리를 사용하기로 했습니다.
라이브러리에 대한 자세한 내용은 react-native-tts를 참고하시면 됩니다.
해당 라이브러리를 설치하고 Tts.speak('Hello, world!');
코드를 이용해 음성이 잘 나오는 지, 테스트 했습니다.
동작은 잘 되었지만, 앱 시작 시 경고가 보입니다.
경고(Warning)
WARN `new NativeEventEmitter()` was called with a non-null argument without the required `addListener` method.
WARN `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method.
원인 찾기
스택 오버플로우에서 찾은 내용으로 추측해 보면,
최신의 React Native 버전을 사용하는데, react-native-tts 라이브러리에는 최신 버전에 필요한 addListener
, removeListeners
메서드가 구현되어 있지 않아서 발생하는 문제로 추정됩니다.
라이브러리 확인
저에게 설치된 버전은 4.1.0 버전이었습니다. react-native-tts는 4.1.1 버전이었습니다. (2023.12.20일 기준)
버전이 맞지 않아 그런가 하여 npm i react-native-tts@4.1.1
명령어를 실행해 봤는데 결과는 찾을 수 없다는 에러가 발생했습니다.
다른 방법을 찾기 위해 라이브러리 코드를 확인해 보았습니다.
라이브러리 코드 확인
제게 설치된 모듈에는 없는 addListener
, removeListeners
메서드가 해당 라이브러리의 Git > TextToSpeechModule.java 코드에는 아래와 같이 구현되어 있었습니다.
@ReactMethod
public void removeListeners(Integer count) {
// Keep: Required for RN built in Event Emitter Calls.
}
@ReactMethod
public void addListener(String eventName) {
// Keep: Required for RN built in Event Emitter Calls.
}
해결
저에게 설치된 react-native-tts 모듈에 위 코드를 직접 추가했습니다.
node_modules/react-native-tts/android/src/main/java/net/no_mad/tts/TextToSpeechModule.java
파일의 끝에 위 코드를 추가했습니다.
⚠ 후에 라이브러리가 업데이트 되거나, npm install로 모듈을 다시 설치하면 해당 코드가 사라질 수 있다는 점 참고해 주세요.
그럼 추가한 코드가 적용되도록 다시 빌드 후 확인하면! 경고가 사라진 것을 확인할 수 있습니다.
Tts.removeEventListener 사용 에러
경고를 해결하고 아래와 같이 코드를 작성 후 확인을 하는데, 에러가 발생했습니다.
useEffect(() => {
const initTts = async () => {
// 생략
};
const eventStart = (event) => {
console.log('start', event);
}
const eventFinish = (event) => {
console.log('finish', event);
}
const eventCancel = (event) => {
console.log('cancel', event);
}
Tts.addEventListener("tts-start", eventStart);
Tts.addEventListener("tts-finish", eventFinish);
Tts.addEventListener("tts-cancel", eventCancel);
Tts.getInitStatus().then(initTts);
return () => {
Tts.removeEventListener("tts-start", eventStart);
Tts.removeEventListener("tts-finish", eventFinish);
Tts.removeEventListener("tts-cancel", eventCancel);
};
}, []);
화면을 나가면서 Tts.removeEventListener
를 호출할 때 아래와 같은 에러가 발생합니다.
ERROR Warning: Internal React error: Attempted to capture a commit phase error inside a detached tree. This indicates a bug in React. Likely causes include deleting the same fiber more than once, committing an already-finishe
d tree, or an inconsistent return pointer.
Error message:
TypeError: undefined is not a function
in Unknown (created by ...)
removeEventListener에서 오류로 기존 이벤트가 삭제 되지 않았기 떄문에, 다음번에 다시 tts가 호출되는 화면으로 들어가면 addEventListener
에 의해 이벤트가 계속해서 중복으로 쌓이게 됩니다.
아래 결과는 4번째로 tts 화면으로 들어가서 Tts.speak를 딱 한 번 호출했을 때의 로그입니다. 4개의 이벤트가 중복으로 쌓여 있는 것을 확인할 수 있습니다.
LOG start {"utteranceId": "-42970200"}
LOG start {"utteranceId": "-42970200"}
LOG start {"utteranceId": "-42970200"}
LOG start {"utteranceId": "-42970200"}
LOG finish {"utteranceId": "-42970200"}
LOG finish {"utteranceId": "-42970200"}
LOG finish {"utteranceId": "-42970200"}
LOG finish {"utteranceId": "-42970200"}
원인 찾기
최신 버전의 React Native에서는 removeEventListener
를 사용할 수 없다는 내용을 찾았습니다.
removeEventListener
was removed in a recent react-native release. You're supposed to call .remove()
on the subscription returned by addEventListener now.
해석: 최신 버전의 React Native에서는 removeEventListener
가 제거되었습니다. 이제 addEventListener로 반환된 구독(subscription)에서 .remove()
를 호출해야 합니다.
해결
Tts.removeEventListener
대신 Tts.addEventListener로 반환된 구독(subscription)에서 .remove()
를 호출하도록 수정했습니다.
useEffect(() => {
const initTts = async () => {
// 생략
};
const eventStart = (event) => {
console.log('start', event);
}
const eventFinish = (event) => {
console.log('finish', event);
}
const eventCancel = (event) => {
console.log('cancel', event);
}
// subscription을 반환 받아서
const subscriptionStart = Tts.addEventListener("tts-start", eventStart);
const subscriptionFinish = Tts.addEventListener("tts-finish", eventFinish);
const subscriptionCancel = Tts.addEventListener("tts-cancel", eventCancel);
Tts.getInitStatus().then(initTts);
return () => {
// 여기서 .remove()를 호출
subscriptionStart.remove();
subscriptionFinish.remove();
subscriptionCancel.remove();
};
}, []);
에러도 해결되고, 화면에서 나갈 때 이벤트도 정상적으로 제거되는 것을 확인할 수 있습니다.
TypeScript 문제
.remove()로 변경 후 동작에는 문제가 없습니다.
하지만 TypeScript에서 빨간 줄이 보이고 TS2339: Property 'remove' does not exist on type 'void'.
에러를 표시합니다.
거슬리니 이것도 해결하기로 합니다.
원인 찾기
해당 모듈을 확인하면 addEventListener
의 실제 반환 타입은 EmitterSubscription 이지만, index.d.ts 파일에는 void로 정의되어 있습니다.
// node_modules/react-native-tts/index.js
// 실제 구현 코드
addEventListener(type, handler) {
return this.addListener(type, handler);
}
// node_modules/react-native-tts/index.d.ts
addEventListener: <T extends TtsEvents>(
type: T,
handler: TtsEventHandler<T>
) => void; // <-- 이 부분이 문제
해결
이 부분은 해당 모듈의 index.d.ts 파일을 직접 수정하지 않고 현재 프로젝트에서 override 하는 방식으로 해결하기로 했습니다.
최상위 폴더에 react-native-tts
폴더를 만들고, 해당 폴더에 index.d.ts
파일을 만들어 아래와 같이 작성했습니다.
(모든 내용은 동일하고 addEventListener
의 반환 타입만 EmitterSubscription
으로 변경했습니다.)
// react-native-tts/index.d.ts
declare module "react-native-tts" {
export class ReactNativeTts extends RN.NativeEventEmitter {
getInitStatus: () => Promise<"success">;
requestInstallEngine: () => Promise<"success">;
requestInstallData: () => Promise<"success">;
setDucking: (enabled: boolean) => Promise<"success">;
setDefaultEngine: (engineName: string) => Promise<boolean>;
setDefaultVoice: (voiceId: string) => Promise<"success">;
setDefaultRate: (rate: number, skipTransform?: boolean) => Promise<"success">;
setDefaultPitch: (pitch: number) => Promise<"success">;
setDefaultLanguage: (language: string) => Promise<"success">;
setIgnoreSilentSwitch: (ignoreSilentSwitch: boolean) => Promise<boolean>;
voices: () => Promise<Voice[]>;
engines: () => Promise<Engine[]>;
/** Read the sentence and return an id for the task. */
speak: (utterance: string, options?: Options) => string | number;
stop: (onWordBoundary?: boolean) => Promise<boolean>;
pause: (onWordBoundary?: boolean) => Promise<boolean>;
resume: () => Promise<boolean>;
addEventListener: <T extends TtsEvents>(
type: T,
handler: TtsEventHandler<T>
) => EmitterSubscription; // <-- 여기만 변경
removeEventListener: <T extends TtsEvents>(
type: T,
handler: TtsEventHandler<T>
) => void;
}
declare const Tts: ReactNativeTts;
export default Tts;
}
참고로, 저는 IntelliJ를 사용하고 있는데, 해당 코드로도 빨간 줄이 제거되지 않아서 File > Invalidate Caches > 모든 옵션 체크 후 > [Invalidate and Restart]
를 해서 캐시를 지우고 나니 깔끔하게 빨간 줄이 사라졌습니다.
결론
- 'node_modules/react-native-tts/android/src/main/java/net/no_mad/tts/TextToSpeechModule.java' 파일에
addListener
와removeListener
를 추가 - removeEventListener 대신
.remove()
방식으로 수정 - '/react-native-tts/index.d.ts' 파일을 override 하여
EmitterSubscription 타입을 반환
하도록 수정
이제 모든 노란 경고와, 빨간 에러, 빨간 줄이 모두 사라졌습니다.