기록
블로그 운영
블로그 작업노트 36: 댓글 이모티콘 기능 확장
블로그 작업노트 36: 댓글 이모티콘 기능 확장
목차
2-3주전쯤에 이미지 댓글기능이라는걸 만들어서 내 사이트 댓글에 적용했다.
그때는 단순히 '관리자'만 댓글에 이미지 경로를 넣어주면 그 이미지를 댓글에 적용할 수 있도록 만든 기능이었다.
다만 자연스럽게 일반 사용자도 이미지를 올렸으면하는 말이 들렸고 이에 따라 이미지 댓글 기능보다는 댓글에 이모티콘을 넣어서 사용할 수 있도록 하는 무언가로 변경하고 싶었다.
그래서 참고한것은 온라인 커뮤니티사이트에 있는 댓글콘 기능이었다.
아카라이브 아카콘 기능참고를 해본건 아카라이브나 개드립에서 볼 수 있는 댓글용 콘 기능이었다.
실제로 커뮤니티에서는 댓글을 직접 타이핑해서 적는다기보다 내가 하고싶거나 전달하고싶은 느낌의 이모티콘 형식의 그림을 올리면서 소통하는 경우가 많다.
변방의 작은 내 사이트에 이런기능을 만들어도 쓸모는 없겠다만 그래도 한번 관련 기능을 구현해보고 싶었고, 내 사이트의 파일관리에 맞는 이모티콘 관리체계도 한번 구현해보고 싶었다.
1. 에디터 파일을 이모티콘으로 사용하기위한 작업
나는 글을 쓸때 업로드하는 모든 파일을 S3서버에 저장해둔다.
그 과정중에 서버에 저장된 파일을 관리하기위해서 별도의 파일 테이블을 아래와 같이 관리하고 있다.
에디터 파일 테이블 구조여기서 이번에 관리를 위해 새로 추가한 컬럼은
is_icon 컬럼이다.이 플래그값이 1일 경우에는 이모티콘 리스트를 띄어주기위한 조회에 걸러지도록 위의 컬럼을 추가했다.
그리고 이렇게 조회된 이모티콘을 분류별로 정리하기 위해서 기존에도 사용했던 description 컬럼을 이용하기로 했다.
순서대로 is_icon | description | created_at 컬럼에 대한 값띄어쓰기를 기반으로 구분되어 첫번째 문자열 단위는 이모티콘의 카테고리를 의미하고 두번째 이후부터가 카테고리 내부의 정렬을 위한 텍스트라고 생각하면된다.
이렇게 정리된 백데이터는 쿼리 정제과정으로 통해 아래처럼 보이게 될것이다.
실제 댓글콘 선택화면, 카테고리(젠레스존제로, 이터널리턴 등)로 이모티콘이 묶여 랜더링된다2. 백데이터 정제
내 에디터 파일은 아래와 같은 여러가지 파일 버전을 가진다.
| 이미지 | 움직이는 이미지(gif, mp4) |
|---|---|
| original | original (gif) |
| optimized | converted |
| thumbnail | poster |
오리지날은 사용자가 올려둔 원본파일을 지칭한다.
예외적으로 동영상 파일이 올라오면 이를 gif로 변환해서 오리지날 파일로 인식한다.
그리고 본문용 이미지로 사용할 optimized 이미지와 converted 영상이 존재하고, 섬네일용 작은 이미지와 비디오 미재생시 보이는 포스터 이미지로 관리된다.
이모티콘 사양에서는 이미지는 thumbnail 을 이용하고 움직이는 이미지는 converted를 이용해야한다.
그리고 이런 데이터들이 위에서 언급한대로 '카테고리'별로 구분되어서 정렬되야한다.
이를 만족하는 쿼리문은 아래와 같이 라라벨에서 쿼리빌더로 구현할 수 있다.
public function fetchCommentIcons(Request $request)
{
// 1. 아이콘 원본 name_origin 가져오기
$origins = EditorFile::where('is_icon', 1)
->where('version', 'original')
->pluck('name_origin');
// 2. description 포함된 original 레코드 목록
$descriptions = EditorFile::whereIn('name_origin', $origins)
->where('version', 'original')
->pluck('description', 'name_origin');
// 3. 썸네일·영상 파일
$files = EditorFile::whereIn('name_origin', $origins)
->whereIn('version', ['converted', 'thumbnail'])
->get();
// 4. 그룹핑 (category -> [file, file, ...])
$grouped = [];
foreach ($files as $file) {
$desc = $descriptions[$file->name_origin] ?? '';
$category = trim(explode(' ', $desc)[0] ?? '기타');
$grouped[$category][] = [
'name_origin' => $file->name_origin,
'version' => $file->version,
'cdn_url' => $file->cdn_url,
'description' => $desc,
];
}
// 5. 그룹 내 정렬: description 기준 정렬
foreach ($grouped as $category => &$items) {
usort($items, function ($a, $b) {
return strcmp($a['description'], $b['description']);
});
}
unset($items); // 참조 해제
// 6. 그룹 자체 정렬: 아이템 수 기준 (많은 순)
uksort($grouped, function ($a, $b) use ($grouped) {
return count($grouped[$b]) <=> count($grouped[$a]);
});
return response()->json(['grouped' => $grouped]);
}Copy나는 플래그나 설명문은 original 레코드에만 저장하기 때문에 먼저 original 레코드의 값을 조회한 후 이후 목적에 맞게 다시한번 thumbnail, converted로 검색을 실행했다.
그리고 그 결과를 하나로 머징한다음 앞단에서 쓸 정보만 추출하고 정렬한 후 앞에 카테고리 기준으로 전달하는 로직을 사용했다.
3. 앞단에서 이모티콘 리스트 랜더링
이렇게 백단에서 데이터를 가져온다면 아래와 같은 스크립트로 앞단에서 첨부파일 리스트를 랜더링할 수 있다.
let iconGridLoadedMap = new WeakMap();
function toggleCommentIcons(button) {
const wrapper = button.closest('.grp_comment_writer');
if (!wrapper) return;
const form = button.closest('form');
if (!form) return;
const nicknameInput = form.querySelector('input[name="nickname"]');
if (nicknameInput && nicknameInput.value.trim() === '') {
toast('이모티콘 사용을 위해 닉네임을 먼저 입력해주세요.', TOAST_WARNING, true);
nicknameInput.focus();
return;
}
const grid = wrapper.querySelector('.comment_icon_grid');
const textarea = wrapper.querySelector('.comment_textarea');
const grpButton = form.querySelector('.grp_button');
const isOpen = grid.style.display === 'block';
if (!isOpen) {
textarea.style.display = 'none';
grid.style.display = 'block';
grpButton.style.display = 'none';
button.classList.add('active');
if (!iconGridLoadedMap.get(grid)) {
fetch('/comment/icons', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
})
.then(res => res.json())
.then(data => {
const grouped = data.grouped;
for (const [group, icons] of Object.entries(grouped)) {
const wrapper = document.createElement('div');
wrapper.className = 'comment_icon_category';
const titleEl = document.createElement('div');
titleEl.className = 'comment_icon_title';
titleEl.textContent = group;
wrapper.appendChild(titleEl);
const groupGrid = document.createElement('div');
groupGrid.className = 'comment_icon_inner_grid';
icons.forEach(icon => {
const isVideo = icon.version === 'converted';
const mediaEl = isVideo ? document.createElement('video') : document.createElement('img');
mediaEl.src = icon.cdn_url;
mediaEl.title = icon.name_origin;
if (isVideo) {
mediaEl.muted = true;
mediaEl.loop = true;
mediaEl.autoplay = true;
mediaEl.playsInline = true;
} else {
mediaEl.alt = '';
}
mediaEl.onclick = function () {
insertIconToken(textarea, icon.name_origin);
toggleCommentIcons(button);
};
groupGrid.appendChild(mediaEl);
});
wrapper.appendChild(groupGrid);
grid.appendChild(wrapper);
}
iconGridLoadedMap.set(grid, true);
const notice = grid.querySelector('.comment_notice');
if (notice) {
grid.appendChild(notice); // 항상 맨 마지막으로 이동
notice.style.display = 'block';
}
});
}
} else {
textarea.style.display = '';
grid.style.display = 'none';
grpButton.style.display = '';
button.classList.remove('active');
}
}
function insertIconToken(textarea, nameOrigin) {
const form = textarea.closest('form');
if (form) {
textarea.value = `{${nameOrigin}}`;
form.requestSubmit(); // onsubmit 핸들러가 실행됨 (addComment 호출됨)
} else {
toast('댓글 이모티콘 처리중 문제가 발생했습니다', TOAST_ERROR);
}
}Copy코드는 길지만 실질적으로 백단에서 받은 그룹, 아이콘 배열을 사용해서 내용을 그려주는 역할을 한다.
실제 구동은 아래와 같다.
이모티콘을 클릭하면 비동기요청으로 백단에서 데이터를 가져와 띄워준다
이렇게 이모티콘 리스트가 노출된 이후 이모티콘을 클릭하면 그 이모티콘의 name_origin값을 폼데이터에 담아 넘기면 백단에서 해당 코드를 적절한 이미지나 동영상 태그로 바꿔주는 역할이라 할 수 있다.
4. 이렇게 만든 이유?
일단 보안적 이유가 강하다.
내가 올려둔 이모티콘만 사용할수있다라는게 결국 사용자로부터 이미지 업로드를 안받겠다는 것과 같은 의미인데, 이런 부분에선 해킹의 시도에 대한 예외처리도 필요하고 이러니저러니 따질부분이 많기때문이다.
더불어 지난번 수정이후 댓글 출력도 이제 html이 처리가 안되기 막았으므로 이렇게 이모티콘으로 입력된 댓글에 한해서만 html 랜더링을 하도록 예외처리해서 내가 지정해둔 방식이외의 인젝션 공격은 자동적으로 차단된다.
뭐 대부분의 분들이 이기능을 사용할린 없겠지만 지인들입장에선 댓글이 놀이터가 될수도 있는부분이기도하고, 내 입자에서도 가끔 달리는 댓글에 감정표현이 필요할경우 긴 텍스트보다 하나의 이미지가 더 어울리는경우도 많긴하다보니...
어찌되었든 만들고싶었던 기능임엔 틀림없긴하다.
생각보다 댓글 부분에 힘을써서그런지 괜시리 잘 사용되었으면 한다는 느낌은 있긴하다만...
뭐 결국 자기만족이지 않을까 싶다!

#블로그
7
개의 댓글
형냐고마워!
4달 전
대댓글
에루샤
4달 전
대댓글
@형냐고마워!
준비
4달 전
대댓글
땅!!!!
4달 전
대댓글
에루샤
4달 전
대댓글
@땅!!!!
ㄱㄱㄱ
3달 전
대댓글
에루샤
3달 전
대댓글
@ㄱㄱㄱ