'hangul-util' 라이브러리 코드를 기반으로 작성된 포스트입니다.
divideHangul('사과')
// ['ㅅ', 'ㅏ', 'ㄱ', 'ㅗ', 'ㅏ']
divideHangul("값싼");
// ['ㄱ', 'ㅏ', 'ㅂ', 'ㅅ', 'ㅆ', 'ㅏ', 'ㄴ']
코드 예시
(전체코드 67줄)
const CHO_HANGUL = [
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ',
'ㄹ', 'ㅁ', 'ㅂ','ㅃ', 'ㅅ',
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ',
'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ',
];
const JUNG_HANGUL = [
'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ',
'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ',
'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ',
'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ',
];
const JONG_HANGUL = [
'', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ',
'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ',
'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ',
'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ','ㅎ',
];
const CHO_PERIOD = Math.floor("까".charCodeAt(0) - "가".charCodeAt(0)); // 588 ( 28 * 21 )
const JUNG_PERIOD = Math.floor("개".charCodeAt(0) - "가".charCodeAt(0)); // 28
const HANGUL_START_CHARCODE = "가".charCodeAt(0);
const HANGUL_END_CHARCODE = "힣".charCodeAt(0);
function isHangul(charCode) {
return HANGUL_START_CHARCODE <= charCode && charCode <= HANGUL_END_CHARCODE;
}
function divideHangul(letter) {
const letterCode = letter.charCodeAt(0);
if (!isHangul(letterCode)) {
return letter;
}
const charCode = letterCode - HANGUL_START_CHARCODE;
const choIndex = Math.floor(charCode / CHO_PERIOD);
const jungIndex = Math.floor((charCode % CHO_PERIOD) / JUNG_PERIOD);
const jongIndex = charCode % JUNG_PERIOD;
return {
cho: CHO_HANGUL[choIndex],
jung: JUNG_HANGUL[jungIndex],
jong: JONG_HANGUL[jongIndex],
};
}
function combine(cho, jung, jong) {
const hangulCode =
HANGUL_START_CHARCODE + cho * CHO_PERIOD + jung * JUNG_PERIOD + jong;
if (!isHangul(hangulCode)) {
return "";
}
return String.fromCharCode(hangulCode);
}
function combineHangul(cho = "", jung = "", jong = "") {
const choIndex = CHO_HANGUL.indexOf(cho);
const jungIndex = JUNG_HANGUL.indexOf(jung);
const jongIndex = JONG_HANGUL.indexOf(jong);
return combine(choIndex, jungIndex, jongIndex);
}
코드 풀이
한글의 초성(19), 중성(21), 종성(28) 조합의 배열을 미리 정의해 줍니다.
// 초성(19개)
const CHO_HANGUL = [
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ',
'ㄹ', 'ㅁ', 'ㅂ','ㅃ', 'ㅅ',
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ',
'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ',
];
// 중성(21개)
const JUNG_HANGUL = [
'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ',
'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ',
'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ',
'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ',
];
// 종성(28개)
const JONG_HANGUL = [
'', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ',
'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ',
'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ',
'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ','ㅎ',
];
유니코드 한글은 44032부터 초성 19개, 중성 21개, 종성 28개로 이루어지고 이를 조합한 11,172개의 문자가 존재합니다.
먼저 미리 초성, 중성, 종성으로 구성된 배열을 만드는 이유는 유니코드만으로는 초성, 중성, 종성을 구분하기 어렵기 때문입니다.
유니코드는 초성, 중성 순서가 아닌 자음, 모음 순서로 구성되어 있기 때문입니다. (ㄱ, ㄲ, ㄴ) X, (ㄱ, ㄲ, ㄳ) O
(유니코드 확인해 보기)
const charCode = 'ㄱ'.charCodeAt(); // 12593
// 원하는 데이터(초성) [ ㄱ, ㄲ, ㄴ, ㄷ, ㄸ ]
// 실제 데이터(종성) [ ㄱ, ㄲ, ㄳ, ㄴ, ㄵ ]
String.fromCharCode(charCode);
// ㄱ
String.fromCharCode(charCode + 1);
// ㄲ
String.fromCharCode(charCode + 2);
// ㄳ
String.fromCharCode(charCode + 3);
// ㄴ
String.fromCharCode(charCode + 4);
// ㄵ
const CHO_PERIOD = Math.floor('까'.charCodeAt(0) - '가'.charCodeAt(0)); // 588 ( 28 * 21 )
const JUNG_PERIOD = Math.floor('개'.charCodeAt(0) - '가'.charCodeAt(0)); // 28
이제 글자를 분리하기 위해서, 한글 유니코드의 패턴을 먼저 알아야 합니다.
1. 초성 규칙
초성 위치는 첫 조합 문자 ‘가’의 위치부터 589 간격마다 다음 초성으로 조합한 문자가 위치합니다. (가 + 589 → 나)
2. 중성 규칙
중성은 각 초성 조합에 29개의 모음 값이 존재합니다. 초성 조합이 바뀔 때마다 다시 처음부터 반복되기 때문에 초성 값을 빼줘야 합니다.
3. 종성 규칙
- 종성은 ‘ㄱ’의 위치부터 ‘ㅎ’까지, 종성이 없는 경우를 포함해 총 28개 값이 존재합니다. (초성은 19개)
한글 검증 코드
const HANGUL_START_CHARCODE = '가'.charCodeAt(0);
const HANGUL_END_CHARCODE = '힣'.charCodeAt(0);
// 조합 된 글자인지 체크 (가 ~ 힣 사이)
function isHangul(charCode) {
return HANGUL_START_CHARCODE <= charCode && charCode <= HANGUL_END_CHARCODE;
}
이제 제작할 분리, 조합 함수에서 사용할 isHangul 함수는 ‘가’(첫 글자) ~ ‘힣’(끝글자) 범위 안에 존재하는지 체크하는 함수입니다.
‘ㄱ’와 ‘가’의 유니코드 시작 위치가 전혀 다르고, 초성구간은 위 수식규칙이 다르기에 예외처리를 따로 해줘야 합니다.
글자 분리
function divideHangul(letter) {
const letterCode = letter.charCodeAt(0);
if (!isHangul(letterCode)) {
return letter;
}
const charCode = letterCode - HANGUL_START_CHARCODE;
const choIndex = Math.floor(charCode / CHO_PERIOD);
const jungIndex = Math.floor((charCode % CHO_PERIOD) / JUNG_PERIOD);
const jongIndex = charCode % JUNG_PERIOD;
return {
cho: CHO_HANGUL[choIndex],
jung: JUNG_HANGUL[jungIndex],
jong: JONG_HANGUL[jongIndex],
};
}
console.log(divideHangul("김"));
// { "cho": "ㄱ", "jung": "ㅣ", "jong": "ㅁ" }
console.log(divideHangul("ㄱ"));
// ㄱ
미리 만들어둔 각 조합의 배열에서 문자를 구하기 위한 Index를 구해줘야 합니다.
1. 초성
- 초성은 589의 간격으로 초성 조합이 바뀝니다. 그러므로 588을 나눠준 값으로 순번을 구해줍니다. (588 나누기)
2. 중성
중성은 초성 조합이 바뀔 때마다 29개의 모음이 존재하니, 초성 위치를 제외한 상태에서 28을 나눠 순번을 구해줍니다. (28 나누기)
3. 종성
종성은 단순히 ‘ㄱ’와 ‘ㅎ’(28개) 위치 사이의 값이므로, 순번을 28의 간격의 나머지로 구해줄 수 있습니다.
글자 결합
// 44032(가) + ((초성 × 21) + 중성) × 28 + 종성
function combine(cho, jung, jong) {
const hangulCode = HANGUL_START_CHARCODE + cho * CHO_PERIOD + jung * JUNG_PERIOD + jong;
if (!isHangul(hangulCode)) {
return '';
}
return String.fromCharCode(hangulCode);
}
console.log(combine(0, 20, 16));
// 김
console.log(combine(-1, 20, 16));
// ''
위에서 초성, 중성, 종성을 구한 방식을 반대로 한 수식으로 위치를 구해주면 문자를 합칠 수 있습니다.
(초성의 순번 * 초성 주기) + (중성의 순번 * 중성의 주기) + 종성 순번 의 수식을 ‘가’의 위치에 더해주면 합쳐진 문자의 위치를 구할 수 있습니다.
function combineHangul(cho = '', jung = '', jong = '') {
const choIndex = CHO_HANGUL.indexOf(cho);
const jungIndex = JUNG_HANGUL.indexOf(jung);
const jongIndex = JONG_HANGUL.indexOf(jong);
return combine(choIndex, jungIndex, jongIndex);
}
const hangul = { cho: 'ㄱ', jung: 'ㅣ', jong: 'ㅁ' };
// const hangul = divideHangul('김');
console.log(combineHangul(hangul.cho, hangul.jung, hangul.jong));
// 김
위에서 미리 선언해 둔 초성, 중성, 종성 배열을 이용해서 순번을 각각 찾아서 assemble() 함수에 넘겨주면, Index가 아닌 문자 결합 기능을 구현할 수 있습니다.
단어 분해, 결합
// 단어 분해
const divideLetter = "수박"
.split("")
.map((letter) => divideHangul(letter));
console.log(divideLetter);
/*
[
{ "cho": "ㅅ", "jung": "ㅜ", "jong": "" },
{ "cho": "ㅂ", "jung": "ㅏ", "jong": "ㄱ" }
]
*/
// 단어 결합
const combineLetter = divideLetter
.map((hangul) => combineHangul(hangul.cho, hangul.jung, hangul.jong))
.join("");
console.log(combineLetter);
// 수박
위에서 제작한 함수들은 모두 ‘한 글자’만 파싱 되도록 구현됐습니다. 단어 또는 문장을 파싱 하기 위해서는 문자를 쪼개서 하나씩 바꿔주는 방법을 사용해 줍니다.