개요
이번에 React Native를 사용해서 일본어 단어 암기 토이 프로젝트를 진행하고 있던 중 한자에 후리가나를 표시해야 하는 상황에 봉착했다.
React나 Vue와 같은 웹 개발 언어는 HTML 5 태그인 <ruby> 태그를 사용해서 간단하게 일본어 한자 위에 후리가나(혹은 요미가나)를 표시할 수 있길래 React Native에서도 비슷한 게 있겠지~. 하는 마음으로 찾아봤지만... 알고 보니 React Native에선 <ruby> 태그와 같은 기능을 하는 게 없었다.
그래서 이번기회에 한자에 후리가나를 표시하는 것을 연구해 보기로 했고 그 과정을 기록해보고자 한다.
연구 시작
일단 만들어보기
import { Text, View, SafeAreaView } from 'react-native';
function Furi({ kanjiList, hiraganaList }) {
return (<View style={{ display: 'flex', flexFlow: 'row', flexWrap: "wrap" }}>
{ kanjiList.map((kanji, index) => {
return <>
<View>
<Text style={{ fontSize: 22 * 0.5}}>
{hiraganaList[index]}
</Text>
<Text style={{ fontSize: 22 }}>{kanji}</Text>
</View>
</>
}) }
</View>)
}
export default function App() {
return (
<SafeAreaView>
<Furi kanjiList={['韓', '国', '人']} hiraganaList={['かん', 'こく', 'じん']} />
<Furi kanjiList={['考', 'え', '方']} hiraganaList={['かんが', ' ', 'かた']} />
<Furi kanjiList={['大', '人', 'し', 'い']} hiraganaList={['おと', 'な', ' ', ' ']} />
<Furi kanjiList={['使', 'い', '方']} hiraganaList={['つか', ' ', 'かた']} />
<Furi kanjiList={['お', '見', '舞', 'い']} hiraganaList={[' ', 'み', 'ま', ' ']} />
</SafeAreaView>
);
}
후리가나와 한자를 한 쌍으로 묶어주고 한자의 수만큼 가로로 이어 붙였다. 이렇게 쌍으로 묶어준 이유는 한자마다 히라가나의 개수가 다르면 히라가나가 계속해서 밀리기 때문에 히라가나를 원하는 위치에 배치할 수 없어서 쌍으로 묶어 후리가나와 한자의 넓이를 동일하게 가져가도록 했다.
한자와 후리가나 간격 조절하기
import { Text, View, SafeAreaView } from 'react-native';
function Furi({ kanjiList, hiraganaList }) {
return (<View style={{ display: 'flex', flexFlow: 'row', flexWrap: "wrap" }}>
{ kanjiList.map((kanji, index) => {
return <>
<View style={{ textAlign: 'center' }}>
<Text style={{ fontSize: 24 * 0.5 }}>
{hiraganaList[index]}
</Text>
<Text style={{ fontSize: 24 }}>{kanji}</Text>
</View>
</>
}) }
</View>)
}
export default function App() {
return (
<SafeAreaView>
<Furi kanjiList={['韓', '国', '人']} hiraganaList={['かん', 'こく', 'じん']} />
<Furi kanjiList={['考', 'え', '方']} hiraganaList={['かんが', ' ', 'かた']} />
<Furi kanjiList={['大', '人', 'し', 'い']} hiraganaList={['おと', 'な', ' ', ' ']} />
<Furi kanjiList={['使', 'い', '方']} hiraganaList={['つか', ' ', 'かた']} />
<Furi kanjiList={['お', '見', '舞', 'い']} hiraganaList={[' ', 'み', 'ま', ' ']} />
</SafeAreaView>
);
}
이전 코드는 후리가나의 위치가 맞는 것 같으면서도 잘 맞지 않는 느낌이 있었다. 후리가나가 두 글자인 경우엔 예쁘게 잘 맞았지만 한 글자인 경우엔 너무 왼쪽으로 치우치고 세 글자 이상인 경우엔 한자 위치에서 오른쪽으로 너무 치우치는 것 같았다.
이 문제를 해결하기 위해서 후리가나와 한자를 감싸고 있는 View 컴포넌트에 textAlign: 'center'를 적용해서 후리가나와 한자에 가운데 정렬을 해줬다. 개인적으론 이전보다 훨씬 보기 좋아진 것 같다.
Props를 객체 배열로 넘기기
import { Text, View, SafeAreaView } from 'react-native';
function Furi({ furiData }) {
return (
<View style={{ display: 'flex', flexFlow: 'row', flexWrap: "wrap" }}>
{ furiData.map(data => (
<View style={{ textAlign: 'center' }}>
<Text style={{ fontSize: 24 * 0.5 }}>
{data.furi ? data.furi : ' '}
</Text>
<Text style={{ fontSize: 24 }}>{data.word}</Text>
</View>
))}
</View>
)
}
export default function App() {
return (
<SafeAreaView>
<Furi furiData={[
{ word: '考', furi: 'かんが' },
{ word: 'え', furi: '' },
{ word: '方', furi: 'かた' }
]} />
<Furi furiData={[
{ word: '送', furi: 'おく' },
{ word: 'り', furi: '' },
{ word: '仮', furi: 'が' },
{ word: '名', furi: 'な' }
]} />
<Furi furiData={[
{ word: '大', furi: 'おと' },
{ word: '人', furi: 'な' },
{ word: 'し', furi: '' },
{ word: 'い', furi: '' }
]} />
<Furi furiData={[
{ word: '大人', furi: 'おとな' },
{ word: 'し', furi: '' },
{ word: 'い', furi: '' }
]} />
</SafeAreaView>
);
}
한자 배열과 한자에 대응되는 후리가나 배열을 각각 Prop으로 넘겨주던 방식을 객체 배열 하나만 넘겨주도록 구조를 살짝 변경했다. 이전 방식 같은 경우엔 길이가 같은 한자 배열과 히라가나 배열이 둘 다 필요했는데 바뀐 방식은 배열 하나만 넘겨주면 되도록 변경해 줌으로써 문장이 길어질 경우 기존 방식보다 꽤나 체감될 정도로 사용하기 수월해질 것 같다.
첫 번째 한자는 left로 정렬하기
import { Text, View, SafeAreaView } from 'react-native';
function Furi({ furiData }) {
return (
<View style={{ display: 'flex', flexFlow: 'row', flexWrap: "wrap" }}>
{ furiData.map((data, index) => (
<View>
<Text style={{ fontSize: 24 * 0.5, textAlign: 'center' }}>
{data.furi ? data.furi : ' '}
</Text>
<Text style={{ fontSize: 24, textAlign: index === 0 ? 'left' : 'center' }}>
{data.word}
</Text>
</View>
))}
</View>
)
}
export default function App() {
return (
<SafeAreaView>
<Furi furiData={[
{ word: '考', furi: 'かんが' },
{ word: 'え', furi: '' },
{ word: '方', furi: 'かた' }
]} />
<Furi furiData={[
{ word: '送', furi: 'おく' },
{ word: 'り', furi: '' },
{ word: '仮', furi: 'が' },
{ word: '名', furi: 'な' }
]} />
<Furi furiData={[
{ word: '大', furi: 'おと' },
{ word: '人', furi: 'な' },
{ word: 'し', furi: '' },
{ word: 'い', furi: '' }
]} />
<Furi furiData={[
{ word: '大人', furi: 'おとな' },
{ word: 'し', furi: '' },
{ word: 'い', furi: '' }
]} />
</SafeAreaView>
);
}
첫 번째 한자에서 후리가나가 3글자 이상이 되어버리면 한자의 넓이보다 후리가나의 길이가 더 넓어지면서 시작 위치가 일정하지 않았다.
첫 글자 뒷부분에 있는 한자들은 간격이 떨어져도 그러려니 할 수 있을 것 같은데 첫 번째 글자는 맞춰주고 싶었다. 그래서 index를 체크하여 첫 번째 글자인 경우에만 한자의 textAlign을 "center"가 아닌 "left"로 적용했다.
긴 문장 테스트
export default function App() {
return (
<SafeAreaView>
<Furi furiData={[
{ word: '5', furi: '' },
{ word: '月', furi: 'がつ' },
{ word: '1', furi: 'つい' },
{ word: '日', furi: 'たち' },
{ word: 'の', furi: '' },
{ word: 'メーデーに、', furi: '' },
{ word: 'パリでは', furi: '' },
{ word: '毎年', furi: 'まいとし' },
{ word: '働', furi: 'はたら' },
{ word: 'く', furi: '' },
{ word: '人', furi: 'ひと' },
{ word: 'たちが', furi: '' },
{ word: 'デモ', furi: 'でも' },
{ word: 'を', furi: 'を' },
{ word: '行', furi: 'おこ' },
{ word: 'なっています。', furi: '' },
]} />
</SafeAreaView>
);
}
긴 문장도 후리가나가 잘 적용되었고 넓이를 넘어서면 줄 개행이 되는 것도 확인했다.
하지만... 가장 큰 문제점은 지금은 단어에 대응되는 furi 값을 직접 설정해 주면 되지만 나중에 DB에 저장하고 불러올 땐 어떻게 대응해야 할지 도저히 감이 잡히지 않는다.
후리가나 표시, 숨기기 만들기
import React from 'react';
import { Text, View, SafeAreaView, Button } from 'react-native';
function Furi({ furiData, isShow = true }) {
return (
<View style={{ display: 'flex', flexFlow: 'row', flexWrap: "wrap" }}>
{ furiData.map((data, index) => (
<View>
<Text style={{ fontSize: 24 * 0.5, textAlign: 'center' }}>
{!data.furi || !isShow ? ' ' : data.furi}
</Text>
<Text style={{ fontSize: 24, textAlign: index === 0 && 'left' }}>
{data.word}
</Text>
</View>
))}
</View>
)
}
export default function App() {
const [isShow, setIsShow] = React.useState(true);
return (
<SafeAreaView>
<Furi furiData={[
{ word: '5', furi: '' },
{ word: '月', furi: 'がつ' },
{ word: '1', furi: 'つい' },
{ word: '日', furi: 'たち' },
{ word: 'の', furi: '' },
{ word: 'メーデーに、', furi: '' },
{ word: 'パリでは', furi: '' },
{ word: '毎年', furi: 'まいとし' },
{ word: '働', furi: 'はたら' },
{ word: 'く', furi: '' },
{ word: '人', furi: 'ひと' },
{ word: 'たちが', furi: '' },
{ word: 'デモ', furi: 'でも' },
{ word: 'を', furi: '' },
{ word: '行', furi: 'おこ' },
{ word: 'なっています。', furi: '' },
]} isShow={isShow} />
<Button
title={isShow ? 'Hide' : 'Show'}
onPress={() => {setIsShow(!isShow)}}
/>
</SafeAreaView>
);
}
지금은 단어 암기가 핵심이라서 문장 전체의 show, hide만 구현했지만 나중에 문장 해석이나 독해와 같은 기능이 필요해지면 특정 단어를 선택했을 경우 보이고 숨기도록 만들어보는 것도 좋을 것 같다.
앱에 임시 적용 해보기
일본어 장음부(ー)나 특정 한자들은 높이가 맞지 않아서 간격이 일정하지 않게 보이는 경우가 있었는데, 폰트 문제였던 것 같다. 앱에 직접 적용해 보면서 천천히 해결해보려고 했는데 생각 이상으로 깔끔하게 나와서 놀랐다.
Furigana NPM 사용
import { Text, View, SafeAreaView } from 'react-native';
const fit = [
{ w: '考', r: 'かんが' },
{ w: 'え', r: 'え' },
{ w: '方', r: 'かた' },
];
function Furi({ writingText, readingText, isShow = true }) {
// const fit = furigana.fit(writingText, readingText, { type: 'object' });
const reg = /[ぁ-ゔ]+|[ァ-ヴー]+[々〆〤]/
return (
<View style={{ display: 'flex', flexFlow: 'row', flexWrap: "wrap" }}>
{ fit.map((data, index) => (
<View>
<Text style={{ fontSize: 24 * 0.5, textAlign: 'center' }}>
{reg.test(data.w) ? ' ' : data.r}
</Text>
<Text style={{ fontSize: 24, textAlign: index === 0 ? 'left' : 'center' }}>
{data.w}
</Text>
</View>
))}
</View>
)
}
export default function App() {
return (
<SafeAreaView>
<Furi
writingText="考え方"
readingText="かんがえかた"
/>
</SafeAreaView>
);
}
Furigana NPM:
https://www.npmjs.com/package/furigana
const fit = furigana.fit("考え方", "かんがえかた", { type: 'object' })
// result
fit = [
{ w: '考', r: 'かんが' },
{ w: 'え', r: 'え' },
{ w: '方', r: 'かた' },
]
Furigana 모듈의 fit() 함수를 호출하면 위와 같은 결과를 받아올 수 있다. 각 한자별로 후리가나와 대응시켜 주는데 덕분에 DB를 어떻게 설계해야 할지 걱정하지 않아도 될 것 같다.
하지만 모듈 사용의 단점은 大人와 같은 단어는 おと/な도 아니고 お/とな도 아닌 두 한자를 묶어서 おとな가 후리가나가 돼야 하지만 결과는 お/とな로 반환된다는 점이다.
즉, 사용하면 매우 편리하긴 하지만 100% 신뢰할 수 없고 해당 라이브러리에 너무 의존하게 될 것 같아서 스토어에 출시해야 하거나 서비스를 해야 한다면 다른 대안점을 찾아봐야 할 것 같다.
후기
레이아웃 개념이 많이 부족한 상태였는데 이번에 일본어 한자에 후리가나 표시 하는 것을 연구해 보면서 Flex에 대한 기본 개념이 조금은 잡힌 것 같다. 하지만 지금 만들고 있는 앱에 다양한 상황에 직접 적용하기에는 개선돼야 하는 점들이 꽤나 보였다. 그 부분들은 앱의 기능들을 구현하면서 조금씩 개선해 나갈 생각이다.