최근 회사의 랜딩 홈페이지를 제작하는 중인데 한 글자씩 나타나는 애니메이션이 포함되어 있다. 안타깝게도 음절 단위가 아닌 음소 단위로 변경되는 것이 보여야 하는 것으로 보인다. 처음에는 직접 하드코딩 하려고 했었지만 치다보니 이건 좀 아닌것 같다는 생각이 들었다.
const ANIMATED_TEXT = [
'ㄱ',
'그',
'그ㄹ',
'그래'
];
그래서 음절을 분리하고 하나씩 조합해서 입력될 수 있도록 만들어 보고자 하였다. 분명 찾아보면 선지자가 있을 것 같았다. 찾아보니 이러한 기능을 제공해 주는 자바스크립트 오픈소스 라이브러리가 있더라. 다만 코드가 다소 복잡해 보여서 원리를 이해하긴 어려웠다.
더 찾아보니 코드가 상당히 깔끔하게 구성된 하나의 글을 발견할 수 있었다. 게다가 열심히 연구한 내용들을 개시해 놓으셔서 매우 편하게 이해하고 필요한 코드를 생성할 수 있었다.
- [자바스크립트] 한글 자음 모음 분리 · 네이버 블로그 @Scripter #
코드는 좀 더 이해하기 쉽도록 변수명만 살짝 바꿨다.
글자 분리기
function 글자_분리기(글자) {
const 초성 = [
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ',
'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ',
'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
];
const 중성 = [
'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ',
'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ',
'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ'
];
const 종성 = [
'', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ',
'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ',
'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ',
'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
];
const 유니코드_한글_시작점 = 44032;
const 글자의_유니코드 = 글자.charCodeAt(0);
const 상대_크기 = 글자의_유니코드 - 유니코드_한글_시작점;
const 초성_인덱스 = parseInt(상대_크기 / 588);
const 중성_인덱스 = parseInt((상대_크기 - (초성_인덱스 * 588)) / 28);
const 종성_인덱스 = parseInt(상대_크기 % 28);
return {
초성: 초성[초성_인덱스],
중성: 중성[중성_인덱스],
종성: 종성[종성_인덱스]
};
}
console.log(글자_분리기('진'));
{ '초성': 'ㅈ', '중성': 'ㅣ', '종성': 'ㄴ' }
규칙은 이렇다. '각'이라는 음절이 등장하는 시점은 유니코드 44032
부터고 이 이후부터 초성은 589
마다 바뀌고 그 안에서 중성은 28
마다 바뀐다. 종성은 28
로 나눈 나머지 값이 된다. 이걸 그대로 이용해서 음소를 배열로 받으면 병합할 수 있는 함수를 만들었다.
글자 병합기
입력값 : ['ㅈ', 'ㅣ', 'ㄴ']
기대값 : '진'
function 글자_병합기(원자들) {
const 초성 = 원자들[0] || '';
const 중성 = 원자들[1] || '';
const 종성 = 원자들[2] || '';
const 초성_유니코드 = 중성.charCodeAt(0);
const 중성_유니코드 = 중성.charCodeAt(0);
const 조성_유니코드 = 중성.charCodeAt(0);
const 모음_유니코드_시작점 = 12598;
const 자음_유니코드_시작점 = 12623;
const 유니코드_한글_시작점 = 44032;
const 초성_인덱스 = 중성_유니코드 - 모음_유니코드_시작점;
const 중성_인덱스 = 중성_유니코드 - 자음_유니코드_시작점;
const 종성_인덱스 = 중성_유니코드 - 모음_유니코드_시작점;
return String.fromCharCode(
유니코드_한글_시작점
+ 초성_인덱스 * 588
+ 중성_인덱스 * 28
+ 종성_인덱스
);
}
console.log(글자_병합기(['ㅈ', 'ㅣ', 'ㄴ']));
ᖹ
결과가 너무 어처구니 없이 나와서 진짜 하나하나 살펴보기 시작했다. 그랬더니 초성이 몇 글자씩 점프가 되는 것을 발견했다. 그리고 나서 알게된 사실은 모든 모음이 초성으로 혹은 종성으로 사용되지 않는다는 점이었다. 즉 저런식으로는 정확한 값을 측정할 수 없었다.
function 글자_병합기(원자들) {
const 초성 = 원자들[0] || '';
const 중성 = 원자들[1] || '';
const 종성 = 원자들[2] || '';
if (!중성) {
return 초성;
}
const 중성_유니코드 = 중성.charCodeAt(0);
const 초성_연결자 = [
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ',
'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ',
'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
].reduce((acc, cur, idx) => ({
...acc,
[cur]: idx
}), {});
const 종성_연결자 = [
'', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ',
'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ',
'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ',
'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
].reduce((acc, cur, idx) => ({
...acc,
[cur]: idx
}), {});
const 자음_유니코드_시작점 = 12623;
const 유니코드_한글_시작점 = 44032;
const 초성_인덱스 = 초성_연결자[초성];
const 중성_인덱스 = 중성_유니코드 - 자음_유니코드_시작점;
const 종성_인덱스 = 종성_연결자[종성];
return String.fromCharCode(
유니코드_한글_시작점
+ 초성_인덱스 * 588
+ 중성_인덱스 * 28
+ 종성_인덱스
);
}
console.log(글자_병합기(['ㅂ', 'ㅞ', 'ㄺ']));
뷁
이제야 정상적으로 결과가 나온다. 다만 위 함수는 실행할때 마다 불필요한 연산을 하므로 reduce
로 생성된 변수를 아래 값으로 교체하거나 아래와 같은 코드를 싫어한다면 변수를 상위 함수에서 선언하도록 하는것이 적합해 보인다.
const 초성_연결자 = {
'ㄱ': 0, 'ㄲ': 1, 'ㄴ': 2, 'ㄷ': 3,
'ㄸ': 4, 'ㄹ': 5, 'ㅁ': 6, 'ㅂ': 7,
'ㅃ': 8, 'ㅅ': 9, 'ㅆ': 10, 'ㅇ': 11,
'ㅈ': 12, 'ㅉ': 13, 'ㅊ': 14, 'ㅋ': 15,
'ㅌ': 16, 'ㅍ': 17, 'ㅎ': 18,
};
const 종성_연결자 = {
'' : 0, 'ㄱ': 1, 'ㄲ': 2, 'ㄳ': 3,
'ㄴ': 4, 'ㄵ': 5, 'ㄶ': 6, 'ㄷ': 7,
'ㄹ': 8, 'ㄺ': 9, 'ㄻ': 10, 'ㄼ': 11,
'ㄽ': 12, 'ㄾ': 13, 'ㄿ': 14, 'ㅀ': 15,
'ㅁ': 16, 'ㅂ': 17, 'ㅄ': 18, 'ㅅ': 19,
'ㅆ': 20, 'ㅇ': 21, 'ㅈ': 22, 'ㅊ': 23,
'ㅋ': 24, 'ㅌ': 25, 'ㅍ': 26, 'ㅎ': 27,
};
직접 함수를 써보니 문제가 많은 것 같네... 실제로 사용할때는 수정하고 써야겠다.
분리기 개선
분리기에서 예외처리 되지 않은 부분이 문제를 일으키는 것으로 보인다. 우선 입력 글자가 한글이 아니면 그 글자 자체를 배열로 리턴하도록 하였고 종성이 없는 경우에는 빈 값이 아닌 초성과 중성만 리턴하도록 변경했다.
function 글자_분리기(글자) {
const 초성 = [
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ',
'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ',
'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
];
const 중성 = [
'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ',
'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ',
'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ'
];
const 종성 = [
'', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ',
'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ',
'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ',
'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
];
const 유니코드_한글_시작점 = 44032;
const 유니코드_한글_종료점 = 55203;
const 글자의_유니코드 = 글자.charCodeAt(0);
if (글자의_유니코드 < 유니코드_한글_시작점 || 글자의_유니코드 > 유니코드_한글_종료점) {
return [ 글자 ];
}
const 상대_크기 = 글자의_유니코드 - 유니코드_한글_시작점;
const 초성_인덱스 = parseInt(상대_크기 / 588);
const 중성_인덱스 = parseInt((상대_크기 - (초성_인덱스 * 588)) / 28);
const 종성_인덱스 = parseInt(상대_크기 % 28);
if (종성[종성_인덱스]) {
return [
초성[초성_인덱스],
중성[중성_인덱스],
종성[종성_인덱스]
];
}
return [
초성[초성_인덱스],
중성[중성_인덱스]
];
}
function shouldBe(func, input, expected) {
console.log(func, input, expected);
try {
const result = func(input);
if (JSON.stringify(expected) === JSON.stringify(result)) {
console.log('✅ 결과가 예상값과 같습니다.');
return true;
}
console.log('❌ 결과가 예상값과 다릅니다. =>', result);
return false;
} catch(e) {
console.log('🟡 알 수 없는 에러 발생. =>', e);
return false;
}
}
(function test() {
shouldBe(글자_분리기, '진', ['ㅈ', 'ㅣ', 'ㄴ']);
shouldBe(글자_분리기, '뷁', ['ㅂ', 'ㅞ', 'ㄺ']);
shouldBe(글자_분리기, '하', ['ㅎ', 'ㅏ']);
shouldBe(글자_분리기, ' ', [' ']);
shouldBe(글자_분리기, 'a', ['a']);
shouldBe(글자_분리기, '.', ['.']);
})();
[Function: 글자_분리기] 진 [ 'ㅈ', 'ㅣ', 'ㄴ' ]
✅ 결과가 예상값과 같습니다.
[Function: 글자_분리기] 뷁 [ 'ㅂ', 'ㅞ', 'ㄺ' ]
✅ 결과가 예상값과 같습니다.
[Function: 글자_분리기] 하 [ 'ㅎ', 'ㅏ' ]
✅ 결과가 예상값과 같습니다.
[Function: 글자_분리기] [ ' ' ]
✅ 결과가 예상값과 같습니다.
[Function: 글자_분리기] a [ 'a' ]
✅ 결과가 예상값과 같습니다.
[Function: 글자_분리기] . [ '.' ]
✅ 결과가 예상값과 같습니다.
Ghost