자바스크립트 한글 음절 분리 / 음소 병합

최근 회사의 랜딩 홈페이지를 제작하는 중인데 한 글자씩 나타나는 애니메이션이 포함되어 있다. 안타깝게도 음절 단위가 아닌 음소 단위로 변경되는 것이 보여야 하는 것으로 보인다. 처음에는 직접 하드코딩 하려고 했었지만 치다보니 이건 좀 아닌것 같다는 생각이 들었다.

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: 글자_분리기] . [ '.' ]
✅ 결과가 예상값과 같습니다.

이 글이 도움이 되었나요?

신고하기
0분 전
작성된 댓글이 없습니다. 첫 댓글을 달아보세요!
    댓글을 작성하려면 로그인이 필요합니다.