서론
학교에서 진행 중인 프로젝트 '주다'는 온·오프라인 학습에서 쓸 수 있는 보조 플랫폼입니다. 방에 입장하기 위한 초대 코드와 출석체크에 필요한 출석 코드를 만들어야 하는데 흔히 쓰이는 알파벳 코드가 아닌 한글 단어로 이루어진 코드를 만들어 보기로 했습니다.
이런 느낌으로요!
🅰 알파벳은 알겠는데... 한글?
알파벳으로만 이루어진 코드는 예전에도 해봤었고, 구글링을 통해 쉽게 자료를 얻을 수 있었습니다.
알파벳을 담은(A-Z, a-z) 문자열 변수를 만들어주고 랜덤 한 인덱스를 뽑은 뒤 이에 매칭 되는 알파벳을 골라주면 됩니다.
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const generateCode = (n: number): string => {
let result = '';
for (let i = 0; i < n; i += 1) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters.charAt(randomIndex);
}
return result;
};
console.log(generateCode(5)); // 5자리 랜덤 코드를 만들어준다.
한글도 같은 방법으로 가능합니다.
다만, 초성, 중성, 종성이 조합되는 한글 특성상 알파벳처럼 문자열(또는 비슷한 데이터)로 담아둘 수 없으므로 유니코드를 이용해보겠습니다. 한글은 유니코드 AC00(44032) ~ D7AF(55215) 범위 안에 들어있습니다. 이를 이용하여 범위 안에 있는 숫자를 랜덤을 뽑고 이를 문자로 바꿔주면 됩니다.
new Array(4)
.fill('')
.map(() => String.fromCharCode(44031 + Math.ceil(Math.random() * 11172)))
.join('');
하지만 진짜 무작위로 만들었기 때문에 "쎚돒샫끟" "볦큹툟븺" 등 사람이 읽기 어려운 형태가 나오게 됩니다.
사람이 쉽게 읽을 수 있으면서 랜덤 단어를 뽑고 싶었기에 해당 방법은 쓸 수 없었습니다.
NLP는 못하니까😅 크롤링으로
NLP(자연어처리)를 이용해서 단어를 만들어낼 수 있었습니다.
파이썬에서는 koNLPy가 유명하였고 쉽게 자료를 찾았습니다. [자료: 랜덤 텍스트 생성하기]
아쉽게도 인공지능 쪽은 기초도 경험도 없었기에 해당 방법을 쓰지는 못했습니다.
이후에 찾은 방법이 사전 크롤링입니다.
크롤링한 단어에서 무작위로 뽑아 재조합하면 새로운 단어가 만들어지게 됩니다. 기존에 있던 단어로 만들었으니 읽기도 쉬워집니다.
국립국어원 우리말샘(opendict.korean.go.kr)에서는 사전 내려받기를 제공합니다. 이를 이용하면 쉽게 단어를 구할 수 있습니다.
❗ 우리말샘과 뒤에서 말할 한국어기초사전은 '크리에이티브 커먼즈 저작자표시-동일조건변경허락 2.0 대한민국 라이선스'를 사용합니다.
사전 데이터는 엑셀과 XML 형태로 내려받을 수 있습니다.
이전에 엑셀 데이터를 다뤄본 경험이 있으므로 엑셀을 이용해 만들어보겠습니다.
JavaScript(TypeScript) 라이브러리인 SheetJS는 엑셀(Sheet) 파싱을 도와주거나 기타 유틸이 포함된 라이브러리입니다.
const wb = read(buffer, { type: "buffer" });
const firstSheet = wb.SheetNames[0];
const sheet = wb.Sheets[firstSheet];
const sheetJson = utils.sheet_to_json(sheet, {
defval: "",
blankrows: true,
});
SheetNames
를 통해 첫 번째 Sheet 이름을 가져오고 Sheets
를 통해 해당 데이터를 가져와줍니다. 자바스크립트(타입스크립트)에서는 json을 사용하는 것이 쉽게 때문에 json 형태로 바꿔주었습니다.
대체 왜 그러는지 모르겠는데, 한글이 아닌 다른 기호(여러 가지 공백들...)가 들어가 있는 경우가 있습니다. 이런 문자들은 제외하기 위해 한글 유니코드 범위를 벗어난 글자가 있으면 제외시켰습니다.
function checkKorean(str: string): boolean {
for (let i = 0; i < str.length; i++) {
if (str[i].charCodeAt(0) > 55215) {
return false;
}
}
return true;
}
사전 데이터를 가져왔기 때문에 사용하기 어려운 단어가 들어있습니다. 품사 중 명사만 가져오고 한 글자인 단어이거나 특수문자(하이픈 등)가 들어간 단어는 제외해보겠습니다.
또한 두 글자 또는 세 글자 단어를 조합하여 만들 계획이기 때문에 네 글자 이상 단어도 제외하겠습니다.
const WORD_COLUNM = "표제어"; // 한국어기초사전 기준
const WORD_PARTS = "품사"; // 한국어기초사전 기준
const filteredWord = sheetJson
.map((i) => {
const item = i as Record<string, string>;
if (item[WORD_COLUNM].length === 1) return undefined;
if (item[WORD_COLUNM].length >= 4) return undefined;
if (item[WORD_PARTS] !== "명사") return undefined;
if (item[WORD_COLUNM].includes("-")) return undefined;
if (!checkKorean(item[WORD_COLUNM])) return undefined;
return item;
})
.filter((i) => i !== undefined)
.map((i) => i && i[WORD_COLUNM]);
동음이의어 등의 이유로 중복된 단어가 들어있습니다. 단어의 뜻은 중요하지 않기 때문에 Set을 이용하여 중복된 단어도 제외하겠습니다.
const uniqueWord = new Set(filteredWord);
const words = [...uniqueWord];
그러면 words
라는 단어가 담긴 배열이 만들어집니다.
그래도 어려운 단어들
우리말샘에서 가져온 단어는 사전에 있는 모든 단어를 가져왔기 때문에 읽기 쉽더라도 생소한 단어가 포함되어 있습니다. 어떻게 해야 될까 고민 중 한국어기초사전(krdict.korean.go.kr)을 제공하고 있었고, 여기도 똑같이 사전 내려받기를 제공했습니다.
데이터의 구조는 똑같고 일부 column만 다르기 때문에 이에 맞춰서 변경 후 다시 완성해줍니다.
💥 Heap Out Of Memory
사전 용량이 만만치 않습니다. 첫 번째 사전 파일이 114MB인데 위에서 만든 스크립트를 실행하면 Heap out of memory가 뜹니다. 해당 오류가 뜨지 않더라도 처리 속도가 느렸기 때문에 해결할 필요가 있습니다. 주원인은 File I/O라고 생각하는데,
- 엑셀 파일 Read
- Buffer을 Sheetjs 객체로 변환하기
- 변환한 객체를 json 형태로 변환하기
1번 문제를 해결하기 위해 Stream을 사용해 파일을 불러왔습니다.
function readSheet(name: string): Promise<WorkBook> {
const buffers: Buffer[] = [];
const readStream = fs.createReadStream(name);
return new Promise((resolve, reject) => {
readStream.on("data", (c: Buffer) => {
buffers.push(c);
});
readStream.on("end", () => {
const buffer = Buffer.concat(buffers);
const wb = read(buffer, { type: "buffer" });
resolve(wb);
});
});
}
이후 console.time()
을 이용해 측정하니까 1번 과정에서 102.452ms, 2번 과정에서 132350.450ms이 측정되었습니다. 3번은 볼 필요도 없이 2번이 문제였습니다.
Sheetjs에서는 별 다른 방법을 제공하지 않았고 있는 방법마저 Pro Edition에서만 지원했습니다. 결국 속도는 포기하고, 자바스크립트에 할당하는 메모리 크기를 늘려 주기로 했습니다.
-max_old_space_size=3072
옵션을 추가하여 실행했더니 오류 없이 성공하였습니다. (옵션은 Byte 단위, 3072 = 3MB)
프로젝트에 이식하기
이제 실제 프로젝트에서 사용하기 위해 가공한 단어를 옮겨보겠습니다. 데이터베이스에 Word
단어 테이블을 만들어줬고 가공한 단어를 넣어줬습니다.
데이터베이스에서 두 단어를 뽑아주고 이를 이어주면 새로운 단어가 만들어집니다.
랜덤 인덱스를 만들기 위해 전체 단어 개수가 필요한데, 랜덤 단어를 만들 때마다 COUNT
쿼리를 쓰기에는 무리가 있어 보입니다. 한번 크롤링, 가공하여 저장한 단어는 변경될 일이 적으므로 단어 개수를 하드코딩 하기로 했습니다.
private WORD_COUNT = 23799;
async get(id: number): Promise<Word> {
const result = await this.userRepository.findOne(id);
if (!result) throw new NotFoundException(WordError.WORD_NOT_FOUND);
return result;
}
async makeRandomWord(): Promise<string> {
const rand1 = Math.floor(Math.random() * this.WORD_COUNT) + 1;
const rand2 = Math.floor(Math.random() * this.WORD_COUNT) + 1;
const word1 = await this.get(rand1);
const word2 = await this.get(rand2);
return `${word1.word}${word2.word}`;
}
해당 코드를 실행해보니까 제가 원하는 결과가 나왔습니다. 해당 코드를 그대로 방 초대코드와 출석체크에 적용하면 끝납니다!
아래는 초대코드에 최종 적용한 모습입니다.
마무리
해당 글에서 소개한 스크립트와 주다 프로젝트는 아래 링크에서 확인할 수 있습니다.
github.com/zzuda/word-preprocessor
'개발 > 개발 후일담' 카테고리의 다른 글
웹에서 즐기는 보드게임 만들기 (0) | 2024.10.08 |
---|---|
NextJS와 Supabase로 포트폴리오 만들어보기 (0) | 2024.09.16 |
학교 인트라넷 프로젝트: 수정과 개발기 (0) | 2021.03.14 |
공적마스크 재고 & 온라인 개학 시간표 알림 챗봇 개발기 (3) | 2020.05.06 |
교내 문제풀이 사이트 개발기 (0) | 2019.10.19 |