๐ฃ ์น ์์ฑ API๋ฅผ ์ฌ์ฉํ๋ ์น ๋ฆฌ๋
16177 ๋จ์ด showdevwebdevtexttospeechjavascript
๋น์ ์ TLDR(๋๋ฌด ๊ฒ์ผ๋ฅด๊ณ ์ฝ์ง ์์)์ ์ํด ์จ๋ผ์ธ ๊ธ์ด๋ ์ ์ฌํ ์น ํ์ด์ง๋ฅผ ์ฝ์ ์ ์ด ์์ต๋๊น?
๋น์ ์ ๋ธ๋ผ์ฐ์ ๊ฐ ๋น์ ์ ๋์ ์ฝ์ ์ ์๊ธฐ๋ฅผ ๋ฐ๋๋๋ค.
์ข์, ๋๋ ์ ๋ง ์ด์ด ์ข๋ค!๋๋ ์น ๋ฆฌ๋ ํ๋๋ฅผ ์ธ์ ๋ค.๐
์ ๋ ฅ์ URL์ด๋ ํ ์คํธ๋ฅผ ๋ณต์ฌํด์ ๋ถ์ฌ๋ฃ์ผ๋ฉด ์ฝ์ ์ ์์ต๋๋ค!
์ต์ ์ฝ๊ธฐ ๊ฐ๋ฅ ์น์ ๐

๐ฌ ์น ์์ฑ API
๋๋ ๋ณธ ๋ธ๋ผ์ฐ์ ์์ ์จ ์น ์์ฑ API์ ์์ฑ ํฉ์ฑ์ ์ฌ์ฉํ๋ค.
์ด๊ฒ์ ์คํ์ ์ธ ๊ธฐ์ ์ด์ง๋ง, ์ง๊ธ ๋น์ ์ ๋ธ๋ผ์ฐ์ ์ ๊ทธ๊ฒ์ด ์์ ๊ฐ๋ฅ์ฑ์ด ๋งค์ฐ ๋๋ค.
์ฌ์ค Chrome 33, Firefox 49, Edge 14 ์ดํ ์ฐ๋ฆฌ๋ ๋ชจ๋ ์ด ๊ธฐ๋ฅ์ ๊ฐ์ง๊ณ ์๋ค.ํ๋ง๊ณ ์น๋ฅผ ์ฌ์ฉํ์ง ์๋๋ก ์ฌ๊ธฐ๋ฅผ ํ์ธํ์ญ์์ค๐ฐ: caniuse Web Speech API .
์์ฑ ์ ๋ ฅ
์ฌ์ฉ์ ์ ๋ ฅ์๋ ๋ค์ HTML ์์๊ฐ ํฌํจ๋ฉ๋๋ค.
textarea
, URL/text ์ฝ๊ธฐselect
์์ฑ ์
๋ ฅrange
ํผ์น ๋ฐ ์๋ ์
๋ ฅ์๋(๋งํ๋ ์๋)๋ 0.5์์ 2 ์ฌ์ด์ด๋ค.
์๋์ด(์๋ฆฌ์ ๋๋ฎ์ด) ๋ฒ์๋ 0์์ 2๊น์ง์ ๋๋ค.
์์ฑ ์ ํ์ ์์คํ ์์ ์ฌ์ฉํ ์ ์๋ ์์ฑ์ ์ ๊ณตํ๋ค.
๐ค ๊ฐ์ฐ ์ข ํฉ ์์ฑ
๊ฐ ์ฅ์น์ ์ฌ์ฉ ๊ฐ๋ฅํ ์์ฑ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
speechSynthesisInstance.getVoices()
. ์ด๊ฒ์ ๋ชจ๋
SpeechSynthesisVoice
๊ฐ์ ๋์์ ๋๋๋ ค์ค๋๋ค. ์ฐ๋ฆฌ๋ ๊ทธ๊ฒ์ ์ ํ ์ต์
์ ์ฑ์๋๋ค.
์ฌ์ฉ์๊ฐ ๋ ์ค ํ๋๋ฅผ ์ ํํ๊ฑฐ๋ ๊ธฐ๋ณธ๊ฐ์ ์ ์งํฉ๋๋ค.
ํ์ฌ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ง์ ์ผ๋ก ๋งํ๋ ์ฌ๋์
SpeechSynthesisUtterance
๋์์ด๋ค.
๐ฃ ๊ฐ์ฐ ์ข ํฉ
SpeechSynthesisUtterance
๋์(utterance
)์ ํ๋์ ๋จ๋
์์ฑ ์์ฒญ๊ณผ ๊ฐ๋ค. ์ฐ๋ฆฌ๋ ๋ฌธ์์ด๋ก ๊ทธ๊ฒ์ ์ด๊ธฐํํ๊ณ ์์ฑ, ์๋, ์์กฐ ๋ฑ ๋ชจ๋ ์์ฑ ์์๋ฅผ ์ถ๊ฐํ๋ค.๋ง์ง๋ง์ผ๋ก
speechSynthesis.speak()
์ ํตํด ์์ฑ์ ํฐ์นํฉ๋๋ค.๋ํ
finishUtteranceCallback
์ ์ ๊ณตํ์ฌ ํ
์คํธ๊ฐ ์์ฑ๋ ๋ ์ฌ์ ๋จ์ถ์ ๊ธฐํ ์ปจํธ๋กค์ ์ฌ์ฉํฉ๋๋ค.์ด ๋ ผ๋ฆฌ๋
speak(string, voice, pitch, rate, finishUtteranceCallback)
์ ๋ด์ธ๋์ด ์๋ค speak(string, voice, pitch, rate, finishUtteranceCallback) {
if (this.synth.speaking) {
console.error('๐ฃ already speaking');
return;
}
if (string) {
const utterance = new SpeechSynthesisUtterance(string);
utterance.onend = () => {
console.log('utterance end');
finishUtteranceCallback();
};
utterance.voice = voice;
utterance.pitch = pitch;
utterance.rate = rate;
this.synth.speak(utterance);
}
}
์ด ๋ชจ๋ ๊ธฐ๋ฅ์ ๋ชจ๋ํ๋ฅผ ์ ์งํ๊ธฐ ์ํด WebSpeechApi
์ ๋ด์ธ๋์ด ์๋ค.๐ฆ์์ฑ์ ๋ํ ์์ธํ ๋ด์ฉ์ MDN Speech Utterance์ ์ฐธ์กฐํ์ญ์์ค.
์ด MDN page์ ์์ฃผ ์ข์ ์๊ฐ ํ๋ ์๋๋ฐ, ๋๋ ๊ทธ๊ฒ์ผ๋ก ๋์ ์์ฉ ํ๋ก๊ทธ๋จ์ ๊ตฌ์ถํ๋ค.๊ฒ์ฌ๋ ํด์ฃผ์ธ์!
๐ URL ํ์ธ
์ฌ์ฉ์๋
textarea
์ URL์ด๋ ํ
์คํธ๋ฅผ ์
๋ ฅํ์ฌ ์ฝ์ ์ ์์ต๋๋ค.๊ทธ๋ฌ๋ URL์ธ์ง ํ์ธํ๋ ๋ฐฉ๋ฒ์ ๋ฌด์์ ๋๊น?
๊ฐ๋จํ
try-catch
ํ๋๋ก ์ด ์ ์ ํ ์ ์๋ค.// simple check if valid URL
try {
new URL(urlOrText);
isUrl = true;
} catch (error) {
// not a URL, treat as string
isUrl = false;
}
์ผ๋ฐ ํ
์คํธ์ ๊ฒฝ์ฐ speak()
์ผ๋ก ์ง์ ์ ๋ฌ๋ฉ๋๋ค.URL์ด ํ์คํ๋ฉด ํ์ด์ง๋ฅผ ๋ถ๋ฌ์ค๊ณ ์ฝ์ ์ ์๋ ์์๋ฅผ ์คํฌ๋ฉํ๊ธฐ ์ํ ์์ฒญ์ ๊ฐ์ ธ์ต๋๋ค.
๐ท๏ธ cheerio์axios๋ฅผ ์ฌ์ฉํ์ฌ ์น ์บก์ฒ
cheerio
์ jQuery์ ์๋ธ์งํฉ์ผ๋ก HTML์ ํด์ํ๋ ๋ฐ ์์ด์ ๋งค์ฐ ๋น ๋ฅด๊ณ ๊ฐ๋จํ๋ฉฐ ์ ์ฐํ๋ค.(์์งํ
cheerio.load(<p>some html</p>)
์ฒ๋ผ ๊ฐ๋จํ๋ค)axios
์ API์์ ๋ด์ฉ์ ๊ฐ์ ธ์ค๋ ๋ฐ ์ฌ์ฉ๋๋ ์ฝ์ ๊ธฐ๋ฐ ํด๋ผ์ด์ธํธ๋ก ์ด ์์์ ์น ํ์ด์ง์์ ์์ ํ HTTP get ์๋ต์ ๊ฐ์ ธ์ต๋๋ค.ํ ๋ง๋๋ก ํ๋ฉด, ์ด๊ฒ์ด ๋ฐ๋ก ๋ด๊ฐ ํ์ด์ง์ ๋ชจ๋ ์ฝ์ ์ ์๋ ์์๋ฅผ ์ป๋ ๋ฐฉ์์ด๋ค.
const getWebsiteTexts = siteUrl => new Promise((resolve, reject) => {
axios
.get(siteUrl)
.then((result) => {
const $ = cheerio.load(result.data);
const contents = $('p, h1, h2, h3').contents(); // get all "readable" element contents
const texts = contents
.toArray()
.map(p => p.data && p.data.trim())
.filter(p => p);
resolve(texts);
})
.catch((err) => {
// handle err
const errorObj = err.toJSON();
alert(`${errorObj.message} on ${errorObj.config.url}\nPlease try a different website`);
urlOrTextInput.value = '';
finishUtterance();
});
});
์ผ๋ถ URL์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ alert()
์ฌ์ฉ์๋ฅผ ํฌ์ฐฉํ์ฌ ํ
์คํธ ์์ญ์ ์ง์ฐ๊ณ ํผ ์
๋ ฅ์ ์ฌ์ค์ ํฉ๋๋ค.์ ์ผ๋ถ URL์ด ์๋ํ์ง ์์ต๋๊น?

โ CORS ์ ์ฑ
์คํฌ๋ ์ดํผ๋ ๋ชจ๋ ์ฌ์ดํธ๋ฅผ ๋ถ์ํ ์ ์์ต๋๋ค.
์ฌ์ค ๋ง์ ์ฌ์ดํธ(๋งค์ฒด ๊ธฐ์ฌ ์๋)์CORS ์ ์ฑ ์ด ์๋ค.
๊ทธ๋์ ์ผ๋ถ ์ฌ์ดํธ์์ ์ด๋ฐ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์๋ค.

CORS policy: No 'Access-Control-Allow-Origin'
์ ๊ฐ์ ์ถ์ฒ์์๋ง ์น ์ฑ ์คํฌ๋ฆฝํธ์์ ์์ฒญ์ ๋ฐ์ ์ ์์์ ๋ํ๋
๋๋ค.cURL
๊ณผPostman์ ์ฌ์ ํ ์ด ์ฌ์ดํธ์์ ์ผํ ์ ์์ผ๋, ์ด๋ ๊ฒ Javascript๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.๋ค์์ ์ข์ CORS ๊ฐ์์ ๋๋ค.

CORS ์ดํด
๋งํด ์คํ๋ฆฌํธใป 2019๋ 11์ 12์ผใป 7๋ถ ์ฝ๊ธฐ
#cors
#javascript
#security
#webdev
dev.to pages work though! Try it out ๐.
Thank you dev.to for allowing us to scrape ๐
โถ๏ธ ์ฌ์, ์ผ์์ ์ง, ์ฌ์์
๋ง์ง๋ง์ผ๋ก ๋๋ ๊ธฐ๋ณธ์ ์ธ ์ฌ์ ์ ์ด๋ฅผ ์ถ๊ฐํ๋ค.

๋ค์์
paused
์ ํ์ฌ speechSyntesis
์ํ์ ๋ฐ๋ผ ์์ํ๊ฑฐ๋ ๋ณต๊ตฌ๋ ์ฌ์ ๊ธฐ๋ฅ์
๋๋ค.์ผ์ ์ค์ง ๋ฐ ์ค์ง๋ฅผ ์ ์ธํ ๋ค๋ฅธ ์ปจํธ๋กค์ disabled
์ ๋ถ๊ณผํฉ๋๋ค.playButton.addEventListener('click', () => {
if (speechApi.synth.paused) {
speechApi.synth.resume();
} else {
// start from beginning
read();
}
playButton.disabled = true;
pauseButton.disabled = false;
stopButton.disabled = false;
rateSlider.disabled = true;
pitchSlider.disabled = true;
voiceSelect.disabled = true;
urlOrTextInput.disabled = true;
});
์ผ์ ์ ์ง๋ ๋ง๋ ์ ๋ ๋น์ทํ์ง๋ง ๋ค๋ฅธ ์ปจํธ๋กค์ ์ฌ์ฉํ์ง ์์ต๋๋ค.๐ฆ ๐ค ๊ตฌ์ถ ๋ฐ ๋ฐฐํฌ
parcel
์ ์ฌ์ฉํ์ฌ ์ค์ ์๋ ๋ฌถ์์ ์งํํฉ๋๋ค. ์ด๊ฒ์ ์ด๋ฌํ vanilla JS ํ๋ก์ ํธ์ ์์ด์ ๋งค์ฐ ๊ฐ๋จํฉ๋๋ค.๋ง์ง๋ง์ผ๋ก Netlify๋ ์ ์ ๋ฐฐํฌ์ ํธ๋ฆฌํฉ๋๋ค.Netlify์์ Github repo๋ฅผ ์ค์ ํ๋ฉด Parcel์์ ๊ตฌ์ถํ
dist/
ํด๋๋ง ์ถ์ถ๋ฉ๋๋ค.์์ฑ!
๐ ๊ฐ๋ํ๋ค
์ด๊ฒ์ ๋น ๋ฅธ ํ๋ก์ ํธ์ด๊ธฐ ๋๋ฌธ์ ํ๋ฆผ์์ด ์ฝ๊ฐ์ ๊ฐ์ ๊ณผ ์์ ์ด ํ์ํ ๊ฒ์ด๋ค.
๐จโ๐ป ์ฝ๋์ ๋๋ค.์ข์ ๋ฌธ์๋ฅผ ์์ฑ ํ๋ก์ ํธ๋ก ์์ํ๋ ๋ฐ ๋์์ด ๋ ์ ์๋ ์์ด๋์ด๋ฅผ ๋ถ๋ฌ์ผ์ผํค๊ธธ ๋ฐ๋๋๋ค.๐
๋ก ๋ชฐ๋ / web_๋ฆฌ๋
์น ์์ฑ API๋ฅผ ์ฌ์ฉํ๋ ์น ๋ฆฌ๋
ํ์ฅ ํ๋ ์ ํ
์ด์
https://stupefied-curran-2254b8.netlify.com/

๊ฐ๋ฐ
npm run dev
๊ฑด์ถํ๋ค
npm run build
View on GitHub
๋ฌด์จ ๊ฑด์, ์๊ฒฌ๊ณผ ๋ฌธ์ ๊ฐ ์์ต๋๊น?
(์: ๋ฌธ์์ด์ด URL์ธ์ง ํ์ธํ๋ ๋ ์ข์ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค๐
)
๋๊ธ๋ก ์๋ ค์ฃผ์ธ์!
๊ณ ๋ง์์, ์ฆ๊ฒ๊ฒ ๋ฃ๊ณ ์ฝ์ด์!๐๐
Reference
์ด ๋ฌธ์ ์ ๊ดํ์ฌ(๐ฃ ์น ์์ฑ API๋ฅผ ์ฌ์ฉํ๋ ์น ๋ฆฌ๋), ์ฐ๋ฆฌ๋ ์ด๊ณณ์์ ๋ ๋ง์ ์๋ฃ๋ฅผ ๋ฐ๊ฒฌํ๊ณ ๋งํฌ๋ฅผ ํด๋ฆญํ์ฌ ๋ณด์๋ค
https://dev.to/lennythedev/web-reader-using-web-speech-api-bkn
ํ
์คํธ๋ฅผ ์์ ๋กญ๊ฒ ๊ณต์ ํ๊ฑฐ๋ ๋ณต์ฌํ ์ ์์ต๋๋ค.ํ์ง๋ง ์ด ๋ฌธ์์ URL์ ์ฐธ์กฐ URL๋ก ๋จ๊ฒจ ๋์ญ์์ค.
์ฐ์ํ ๊ฐ๋ฐ์ ์ฝํ
์ธ ๋ฐ๊ฒฌ์ ์ ๋
(Collection and Share based on the CC Protocol.)
Reference
์ด ๋ฌธ์ ์ ๊ดํ์ฌ(๐ฃ ์น ์์ฑ API๋ฅผ ์ฌ์ฉํ๋ ์น ๋ฆฌ๋), ์ฐ๋ฆฌ๋ ์ด๊ณณ์์ ๋ ๋ง์ ์๋ฃ๋ฅผ ๋ฐ๊ฒฌํ๊ณ ๋งํฌ๋ฅผ ํด๋ฆญํ์ฌ ๋ณด์๋ค https://dev.to/lennythedev/web-reader-using-web-speech-api-bknํ ์คํธ๋ฅผ ์์ ๋กญ๊ฒ ๊ณต์ ํ๊ฑฐ๋ ๋ณต์ฌํ ์ ์์ต๋๋ค.ํ์ง๋ง ์ด ๋ฌธ์์ URL์ ์ฐธ์กฐ URL๋ก ๋จ๊ฒจ ๋์ญ์์ค.
์ฐ์ํ ๊ฐ๋ฐ์ ์ฝํ
์ธ ๋ฐ๊ฒฌ์ ์ ๋
(Collection and Share based on the CC Protocol.)
์ข์ ์นํ์ด์ง ์ฆ๊ฒจ์ฐพ๊ธฐ
๊ฐ๋ฐ์ ์ฐ์ ์ฌ์ดํธ ์์ง
๊ฐ๋ฐ์๊ฐ ์์์ผ ํ ํ์ ์ฌ์ดํธ 100์ ์ถ์ฒ ์ฐ๋ฆฌ๋ ๋น์ ์ ์ํด 100๊ฐ์ ์์ฃผ ์ฌ์ฉํ๋ ๊ฐ๋ฐ์ ํ์ต ์ฌ์ดํธ๋ฅผ ์ ๋ฆฌํ์ต๋๋ค