[DB] 문자열 데이터 타입에 글자 수 제한하기

ziwww·2024년 2월 26일

개발

목록 보기
5/14

Intro

게시글 저장하는 기능을 구현하기 위해 post 테이블을 이런식으로 생성하였다.

이것저것 저장 테스트를 해보니 "Data too long for column" 오류가 발생한다.
정해진 컬럼의 길이보다 저장할 데이터가 커서 다음과 같은 오류가 발생한 것.
이를 해결하기 위해 글자 수 제한 기능을 추가할 것이다.

게시글을 저장할 때 제목은 250, 문제 문자열은 3000, 정답 문자열은 3000이 넘으면 안된다.

UTF-8

UTF-8은 유니코드 문자를 인코딩하는 가변 길이 문자 인코딩 방식 중 하나로, 8비트 기반의 문자 인코딩 방식이다.
ASCII문자(영어, 기호,...)는 1byte로 표현하고, 한글과 같은 유니코드 문자는 3바이트로 표현한다.

따라서 varchar(10)일때는 한글이 4글자 이상 들어가면 Data too long for column 오류가 나올 것이다.

이게 맞는지 한 번 테스트를 해보았다.
확인 해보기 위해 title을 10으로 제한한 후, 한글을 4글자 적어주었다.


하지만, 예상과 달리 정상적으로 저장이 되었다.

알고보니 char 및 varchar 타입은 최대 문자 수를 나타내는 길이로 선언되는 것이었다.
예를 들어 varchar(10)은 10byte가 아닌 최대 10자까지 입력할 수 있다는 의미이다.

하지만 줄 바꿈을 하면 10글자가 들어가지 않는다.

분명히 12345+줄바꿈+6789해서 10개일텐데 말이다.
원인은 잘 모르겠지만 아마 줄바꿈을 (\n)로 인식하기 때문에 줄바꿈은 2글자로 인식하는 것으로 보인다.
(나는 maria db 사용했기 때문에 다른 데이터베이스는 다를 수 있다. 오라클에서는 한글을 3바이트로 인식한다는 말이 있다.)

결론

maria db에서 varchar(길이)는 byte가 아니라 글자 단위 길이이다.
줄 바꿈은 \n이기 때문에 2글자로 표현된다.


글자 수 제한 기능 추가하기

데이터베이스에서 어떤 식으로 저장되는 지 파악했기 때문에 기능을 만들 수 있다.
글자 수 제한하는 기능을 만들어보자!

input값을 실시간 감지하여 value를 가져와서 글자 수를 세줄 것이다.
사용자가 입력할 때(input), input값이 바뀔 때(change) 모두 이벤트 처리를 해줘야 하기 때문에 jQuery를 사용하였다.

출처: https://karismamun.tistory.com/66

const titleByteLimit = 250;

const titleTextCount=document.querySelector('.title-text-count');
$(document).ready(function() {
    $('.title').on('change input', function() {
        // Assuming countBytes is a function that you have defined elsewhere
        countBytes(this, titleTextCount, titleByteLimit);
        autoResize(this);
    });
});
  • titleTextCount는 글자수를 표시하는 div이다.
  • titleByteLimit는 제한 할 글자 수이다.
  • autoResize는 textarea의 높이 길이를 글자 크기만큼 늘려주는 function

글자 수 세기

function countUtf8Bytes(str) {
    let byteCount = 0;

    for (let i = 0; i < str.length; i++) {
        const charCode = str.charCodeAt(i);
        //줄바꿈일 경우 바이트2, 아니라면 1
        (charCode==10) ? byteCount+=2: byteCount+=1;
    }

    return byteCount;
}
  • str은 사용자가 입력한 value값
  • value값을 첫번째 문자부터 끝 문자까지 하나씩 돌면서 str.charCodeAt(i)로 유니코드를 가져온다.
  • 만약 유니코드가 10이라면 2글자 취급이니 2를 더해준다. (줄 바꿈의 유니코드는 10이다.)
  • 10이 아니라면 1글자 취급을 하니 1을 더해준다.

글자 수를 세는 function을 만들었으니, 이번엔 글자 수가 제한을 넘을 경우 제한된 길이만큼 문자를 자르는 기능을 만들어야한다.

개선된 for문 로직 분석

for (let b = i = 0; c = str.charCodeAt(i);) {
  ...
}
  • let b = i = 0 : b와 i를 0으로 초기화
  • c = str.charCodeAt(i): 변수 c에 문자열 str에서 인덱스 i에 해당하는 문자 유니코드 할당
  • 만약 스트링 전체를 탐색했으면 charCodeAt()를 사용할 때 실패하고 for문이 종료 된다.
  • c에 현재 위치의 유니코드 값을 가져왔다면, 바이트 계산을 진행하면 된다.

문자열 자르기

function cutByLen(str, maxByte) {
    for (let b = i = 0; c = str.charCodeAt(i);) {
        //줄 바꿈(/n)일 경우 byte 2, 나머지 1
        b+=c==10?2:1;
        if (b > maxByte) break;
        i++;
    }
    return str.substring(0, i);
}
  • 만약 c가 10일 때는 줄 바꿈이기 때문에 2글자로 취급하기 때문에 2를 더한다.
  • 10이 아니라면 1글자 취급해야하기 때문에 1을 더한다.
  • 줄 바꿈이 2글자로 취급되지만 이것은 byte계산이고 실제로는 하나의 문자이기 때문에 i는 1만 더해준다.
  • 이렇게 하면 varchar이 3000인 문자열에 몇 개의 문자가 들어갈 수 있는지 구할 수 있다.
    • 예) 줄 바꿈이 없는 문자열일 경우, i는 3000이다. (문자가 3000개 들어감)
    • 예) 줄 바꿈이 1개 있는 문자열일 경우, i는 2999이다.(문자가 2999개 들어감)
  • 들어갈 수 있는 문자만큼 substring으로 자른다. (넘치는 문자열은 모두 잘린다.)

이렇게 바이트 수 세는 함수, 바이트 수가 넘칠 경우 문자열을 자르는 함수를 만들었으니 이제 이것들을 적용하는 함수를 만들 것이다.

만든 함수들 적용하기

function countBytes(editor, containerSelector, limit) {
    //toast api일땐 getMarkdown 그냥 input일 경우 value
    const content = editor.getMarkdown ? editor.getMarkdown() : editor.value;
    //문자열을 바이트로 변환
    const byteCount =countUtf8Bytes(content);

    containerSelector.textContent = byteCount + "/" + limit;

    // 제한을 넘으면 에디터 사용 막기
    if (byteCount > limit) {
        alert('허용된 글자수가 초과되었습니다.')
        const truncatedContent = cutByLen(content, limit);
        editor.setMarkdown ? editor.setMarkdown(truncatedContent,false) : editor.value = truncatedContent; // 넘어간 부분을 잘라냄
    }
}
  • editor.getMarkdown ? editor.getMarkdown() : editor.value: getMarkdown으로 조건식을 쓴 이유는, toast api editor 안에 있는 문자열은 getMarkdown으로 가져오고, 정상적인 textarea에 있는 문자열은 value로 가져오기 때문
    • 그렇기 때문에 getMarkdown이 가능한 지로 textarea인지 toast api editor인지 구분한다.
  • 가져온 문자열을 byte 계산해준다.
  • 계산이 끝났다면, div의 text를 계산된 값으로 바꿔준다.
  • 만약 계산된 값이 제한된 값보다 크다면, alert 창을 띄워주고 문자열을 잘라준다.
    • setMarkdown으로 조건식을 쓴 이유도 위의 이유와 동일하다.

이렇게 글자 수 제한하는 기능들을 다 만들었으니 사용하고 싶은 곳에 적용해주면 된다.


적용하기

toast api editor

const byteLimit = 3000;
const quizTextCount=document.querySelector('.quiz-text-count');

const contentEditor = new toastui.Editor({
    el: document.querySelector('.quiz-content'), // 에디터를 적용할 요소 (컨테이너)
    height: '700px',                        // 에디터 영역의 높이 값 (OOOpx || auto)
    initialEditType: 'markdown',            // 최초로 보여줄 에디터 타입 (markdown || wysiwyg)
    initialValue: quizContent,                       // 내용의 초기 값으로, 반드시 마크다운 문자열 형태여야 함
    previewStyle: 'vertical', // 마크다운 프리뷰 스타일 (tab || vertical)
    placeholder: '퀴즈 문제를 내주세요.',
    autofocus: false,
    events: {
        change: function () {
            //wordCount.js 파일
            countBytes(contentEditor, quizTextCount, byteLimit);
        }
    },

change는 에디터에 들어있는 문자열이 달라질 때마다 실행되는 function이다.
문자열이 달라질 때마다(사용자가 문자를 입력할 때마다) 글자 수 제한 기능이 실행된다.





댓글에서의 글자 수 제한 기능

댓글은 하나의 input만 있다. ('comment-text-count'가 하나만 존재하기 때문에 querySelector로 가져올 수 있음)

하지만 대댓글은 여러개의 input이 존재한다. ('re-comment-text-count'가 여러개 존재하기 때문에 querySelector로 가져올 수 없음)

따라서 해당 대댓글의 re-comment-text-count를 따로 가져오는 함수가 필요하다.

function commentWordCount(textarea){
    const commentTextCount = document.querySelector('.comment-text-count');
    countBytes(textarea, commentTextCount, 1500);
}

function reCommentWordCount(textarea){
    const reCommentForm= textarea.closest('.re-comment-form');
    const reCommentTextCount = reCommentForm.querySelector('.re-comment-text-count');
    countBytes(textarea, reCommentTextCount, 1500);
}
  • comment는 하나이기 때문에 querySelector로 comment-text-count div 요소를 가져온다.
  • recomment는 여러개이므로 이벤트가 발생되어야 하는 요소를 querySelector로 가져오기 쉽지않다.
    따라서 html 태그에서 function을 입력하는 방식으로 사용 할 것이다.

function reCommentWordCount(textarea){
    const reCommentForm= textarea.closest('.re-comment-form');
    const reCommentTextCount = reCommentForm.querySelector('.re-comment-text-count');
    countBytes(textarea, reCommentTextCount, 1500);
}
  • textarea.closest('.re-comment-form'): 이벤트가 발생한 요소에서 시작하여 상위로 이동하면서, 주어진 필터 조건을 만족하는 가장 가까운 조상 요소를 반환한다. (사용자가 입력하고 있는 대댓글 입력 칸 찾기)
  • 그 부모 요소에서 queryselector로 '.re-comment-text-count' div요소를 가져온다.
  • 가져온 div를 이용하여 글자 수 제한하는 함수 쓰기

<textarea class="answer-input" placeholder="정답을 작성해보세요" rows="30"
          th:field="${newComment.contents}"
          onchange="commentWordCount(this);"
          oninput="commentWordCount(this);"></textarea>
  • onchange와 oninput에 글자 수 제한 함수를 넣어줘서 값들이 변경될 때마다 이벤트가 발생한다.
  • 만약 이벤트가 발생하면 이벤트가 발생한 요소를 매개변수로 전달한다.

이렇게 코드를 작성하면 사용자가 작성하고 있는 form에서만 기능이 동작하는 것을 확인 할 수 있다.

profile
반갑습니다. 오늘도 즐거운 하루입니다.

0개의 댓글