ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이모지와 유니코드 그리고 자바
    개발 나누고 더하기/자바, 스프링 2023. 4. 22. 00:09

    발단


    2021년 어느날 회사 서비스의 DB 컬레이션을 utf8mb4로 수정하였다. 회사 내 다른 서비스와 상품 연동을 하는데, 이 채널로부터 4bytes 이모지가 포함된 상품 데이터를 받아 해당 상품 페이지의 이모지가 다 깨져 나왔기 때문이다. 우리는 DB 컬레이션만 수정하면 되는데 문제는 우리와 상품 연동을 하는 또 제 3의 서비스에서 이모지 허용이 안되는 것이 문제였다.

    이모지를 걸러서 전송해야 하는데 2가지 방향으로 문제를 풀 수 있다. 화이트 리스트를 만들어 이 안에 포함된 문자만 허용하거나 블랙 리스트를 만들어 그 안에 든 문자들을 제거하는 것이다. 그 당시 이 문제를 담당했던 개발자는 전자를 택하여 아래와 같은 코드가 나왔다.

    public class EmojiUtil {
        private static final String PATTERN = "[^\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\p{Cf}\\p{Cs}\\s]";
    
        public static String removeEmoji(String text) {
            return text.replaceAll(PATTERN, "");
        }
    }

    L(Letter), M(Mark), N(Number) 등이 아닌 문자들을 거른다는 뜻이다. 처음에 잘 되는줄 알고 안심했으나 1년 반이 지나고 난 뒤 허점이 있음을 알아차렸다. 아래와 같이 몇몇 이모지들이 걸러지지 않는 문제가 발생한 것이다. 역시 전수 조사를 했어야 했나...

    불의의 일격을 당한 테스트 코드

    아쉽게도 전임자는 퇴사했고, 이 문제는 내가 정면으로 상대해야 했다.

     

    이모지 배경 이해 - 유니코드


    뭐 정규식 살짝 바꾸면 되겠지 하고 구글과 챗GPT님께 여쭤봤으나 원하는 자료가 많이 없었다. 그래서 이참에 내가 자료를 만들기로 하고 이모지에 대해 좀 더 파보았다. 

    이모지는 unicode.org(https://home.unicode.org/)에서 관리한다. 이모지도 문자라 유니코드로 되어있기 때문이다. 2023년 4월 기준으로 3,664개가 등록되어있고, https://unicode.org/emoji/charts/emoji-counts.html 여기서 확인 가능하다. 신규 이모지는 unicode.org에 신청(2024년 4월에 재오픈한다고 함)하고 심사 통과되면 등록된다. 이 때 등록된 이모지와 여러 문자들이 유니코드 버전과 함께 배포된다. https://unicode.org/Public/ 여기에서 유니코드 배포 이력을 확인 할 수 있고 현재 최신은 15버전이다.

    유니코드 배포 버전

    여기까지 알아봤으면 이제 수많은 문자중 이모지를 식별하기 위해 유니코드 어디에 분포되어 있는지를 알아야 한다. 이를 위해 유니코드 평면과 시퀀스에 대해 알아야 한다.

     

    유니코드 그 어딘가에 이모지, 평면과 시퀀스

    가로축과 세로축을 조합해 하나의 코드로 찾아가기 위한 좌표계 정도로 이해하면 된다. 유니코드 전체를 논리적으로 나눈 구획을 말하며 0번(다국어 기본 평면)에서부터 16번까지 모두 17개로 나뉘고, 각 평면은 65,536(2의 16승)개의 코드로 구성된다. 모두 다 알 필요는 없고 기본 평면과 보조 평면 2개만 알면 된다.

    기본 평면(BMP, Basic Multilingual Plane)은 가장 기본이 되는 문자집합을 포함하며 U+0000~U+FFFF까지의 유니코드를 사용한다. 아스키코드 문자(영어, 숫자 등)과 CJK(한국어, 중국어, 일본어)와 같은 각종 언어 문자들 및 소수의 이모지들이 배치되어 있다. 보조 평면(SMP, Supplementray Multilingual Plane)은 기본 평면 외 보조적으로 사용하는 문자집합을 포함하고 U+10000~U+1FFFF까지의 유니코드를 사용한다. 대부분의 이모지가 이곳에 배치되어 있다.

    BMP - 출처 : https://unicode.org/roadmaps/bmp/

    좋아 그럼 간단하게 이모지 유니코드 최소~최대 범위로 거르면 되겠네? 할 수 있겠지만 그렇지 않다. 먼저 이모지가 유니코드 평면에서 아주 띄엄띄엄 분포해있다. 그리고 유니코드 코드 포인트 1개 당 이모지 1개의 1:1 관계가 아니라 여러 코드포인트로 1개의 이모지를 만드는 경우도 있기 때문에 그러한 경우를 다 고려해야 한다. 이를 해결하기 위한 힌트는 https://unicode.org/Public/emoji/15.0/emoji-sequences.txt 에서 찾을 수 있다. 

    Basic_Emoji
    Emoji_Keycap_Sequence
    RGI_Emoji_Flag_Sequence
    RGI_Emoji_Flag 한국

    다행스러운건 유니코드가 아래 5개의 그룹으로 이모지들을 분류해놓았고, 각 그룹에 속한 이모지들은 인접한 유니코드를 사용하고 조합 패턴도 비슷하다.

    • Basic_Emoji : 대부분 1개의 유니코드, 소수?의 2개로 조합된 유니코드를 사용한다.
    • Emoji_Keycap_sequence : 3개의 유니코드로 조합되는데 맨 앞은 아스키코드 값, 중간과 끝은 FF0F 20E3으로 키캡 모양과 색깔을 나타낸다.
    • RGI_Emoji_Flag : Region_Indicator의 조합으로 한국(KR)을 예를들면 1F1F0(K)와 1F1F7(R)을 조합한 것이다.
    • RGI_EMoji_Tag_sequence : 영국 본토에 있는 영연방 국가들의 국기로 링크따라 확인하면 그룹내 같은 패턴을 보인다.
    • RGI_Emoji_Modifier_sequence : 사람 모션이 주로 있는데 역시 그룹내 같은 패턴으로 되어있다.

    해결 - 무식한 방법?


    위 5개 그룹별로 정규식 패턴을 만들어 조합한 코드를 만들었다.

    public class EmojiUtil {
        private static final String BASIC_EMOJI_SINGLE_UNICODE_PATTERN = "[\\x{1F004}\\x{1F0CF}" +
                "\\x{1F18E}\\x{1F191}-\\x{1F19A}" +
                "\\x{1F201}\\x{1F21A}\\x{1F22F}\\x{1F232}-\\x{1F236}\\x{1F238}-\\x{1F23A}\\x{1F250}-\\x{1F251}" +
                "\\x{1F300}-\\x{1F30C}\\x{1F30D}-\\x{1F30E}\\x{1F30F}\\x{1F310}\\x{1F311}\\x{1F312}\\x{1F313}-\\x{1F315}\\x{1F316}-\\x{1F318}\\x{1F319}\\x{1F31A}\\x{1F31B}\\x{1F31C}\\x{1F31D}-\\x{1F31E}\\x{1F31F}-\\x{1F320}\\x{1F32D}-\\x{1F32F}\\x{1F330}-\\x{1F331}\\x{1F332}-\\x{1F333}\\x{1F334}-\\x{1F335}\\x{1F337}-\\x{1F34A}\\x{1F34B}\\x{1F34C}-\\x{1F34F}\\x{1F350}\\x{1F351}-\\x{1F37B}\\x{1F37C}\\x{1F37E}-\\x{1F37F}\\x{1F380}-\\x{1F393}\\x{1F3A0}-\\x{1F3C4}\\x{1F3C5}\\x{1F3C6}\\x{1F3C7}\\x{1F3C8}\\x{1F3C9}\\x{1F3CA}\\x{1F3CF}-\\x{1F3D3}\\x{1F3E0}-\\x{1F3E3}\\x{1F3E4}\\x{1F3E5}-\\x{1F3F0}\\x{1F3F4}\\x{1F3F8}-\\x{1F407}" +
                "\\x{1F408}\\x{1F409}-\\x{1F40B}\\x{1F40C}-\\x{1F40E}\\x{1F40F}-\\x{1F410}\\x{1F411}-\\x{1F412}\\x{1F413}\\x{1F414}\\x{1F415}\\x{1F416}\\x{1F417}-\\x{1F429}\\x{1F42A}\\x{1F42B}-\\x{1F43E}\\x{1F440}\\x{1F442}-\\x{1F464}\\x{1F465}\\x{1F466}-\\x{1F46B}\\x{1F46C}-\\x{1F46D}\\x{1F46E}-\\x{1F4AC}\\x{1F4AD}\\x{1F4AE}-\\x{1F4B5}\\x{1F4B6}-\\x{1F4B7}\\x{1F4B8}-\\x{1F4EB}\\x{1F4EC}-\\x{1F4ED}\\x{1F4EE}\\x{1F4EF}\\x{1F4F0}-\\x{1F4F4}\\x{1F4F5}\\x{1F4F6}-\\x{1F4F7}\\x{1F4F8}\\x{1F4F9}-\\x{1F4FC}\\x{1F4FF}-\\x{1F502}" +
                "\\x{1F503}\\x{1F504}-\\x{1F507}\\x{1F508}\\x{1F509}\\x{1F50A}-\\x{1F514}\\x{1F515}\\x{1F516}-\\x{1F52B}\\x{1F52C}-\\x{1F52D}\\x{1F52E}-\\x{1F53D}\\x{1F54B}-\\x{1F54E}\\x{1F550}-\\x{1F55B}\\x{1F55C}-\\x{1F567}\\x{1F57A}\\x{1F595}-\\x{1F596}\\x{1F5A4}\\x{1F5FB}-\\x{1F5FF}" +
                "\\x{1F600}\\x{1F601}-\\x{1F606}\\x{1F607}-\\x{1F608}\\x{1F609}-\\x{1F60D}\\x{1F60E}\\x{1F60F}\\x{1F610}\\x{1F611}\\x{1F612}-\\x{1F614}\\x{1F615}\\x{1F616}\\x{1F617}\\x{1F618}\\x{1F619}\\x{1F61A}\\x{1F61B}\\x{1F61C}-\\x{1F61E}\\x{1F61F}\\x{1F620}-\\x{1F625}\\x{1F626}-\\x{1F627}\\x{1F628}-\\x{1F62B}\\x{1F62C}\\x{1F62D}\\x{1F62E}-\\x{1F62F}\\x{1F630}-\\x{1F633}\\x{1F634}\\x{1F635}\\x{1F636}\\x{1F637}-\\x{1F640}\\x{1F641}-\\x{1F644}\\x{1F645}-\\x{1F64F}\\x{1F680}\\x{1F681}-\\x{1F682}\\x{1F683}-\\x{1F685}\\x{1F686}\\x{1F687}\\x{1F688}\\x{1F689}\\x{1F68A}-\\x{1F68B}\\x{1F68C}\\x{1F68D}\\x{1F68E}\\x{1F68F}\\x{1F690}\\x{1F691}-\\x{1F693}\\x{1F694}\\x{1F695}\\x{1F696}\\x{1F697}\\x{1F698}\\x{1F699}-\\x{1F69A}\\x{1F69B}-\\x{1F6A1}\\x{1F6A2}\\x{1F6A3}\\x{1F6A4}-\\x{1F6A5}\\x{1F6A6}\\x{1F6A7}-\\x{1F6AD}\\x{1F6AE}-\\x{1F6B1}\\x{1F6B2}\\x{1F6B3}-\\x{1F6B5}\\x{1F6B6}\\x{1F6B7}-\\x{1F6B8}\\x{1F6B9}-\\x{1F6BE}\\x{1F6BF}\\x{1F6C0}\\x{1F6C1}-\\x{1F6C5}\\x{1F6CC}\\x{1F6D0}\\x{1F6D1}-\\x{1F6D2}\\x{1F6D5}\\x{1F6D6}-\\x{1F6D7}\\x{1F6DC}\\x{1F6DD}-\\x{1F6DF}\\x{1F6EB}-\\x{1F6EC}\\x{1F6F4}-\\x{1F6F6}\\x{1F6F7}-\\x{1F6F8}\\x{1F6F9}\\x{1F6FA}\\x{1F6FB}-\\x{1F6FC}" +
                "\\x{1F7E0}-\\x{1F7EB}\\x{1F7F0}" +
                "\\x{1F90C}\\x{1F90D}-\\x{1F90F}\\x{1F910}-\\x{1F918}\\x{1F919}-\\x{1F91E}\\x{1F91F}\\x{1F920}-\\x{1F927}\\x{1F928}-\\x{1F92F}\\x{1F930}\\x{1F931}-\\x{1F932}\\x{1F933}-\\x{1F93A}\\x{1F93C}-\\x{1F93E}\\x{1F93F}\\x{1F940}-\\x{1F945}\\x{1F947}-\\x{1F94B}\\x{1F94C}\\x{1F94D}-\\x{1F94F}\\x{1F950}-\\x{1F95E}\\x{1F95F}-\\x{1F96B}\\x{1F96C}-\\x{1F970}\\x{1F971}\\x{1F972}\\x{1F973}-\\x{1F976}\\x{1F977}-\\x{1F978}\\x{1F979}\\x{1F97A}\\x{1F97B}\\x{1F97C}-\\x{1F97F}\\x{1F980}-\\x{1F984}\\x{1F985}-\\x{1F991}\\x{1F992}-\\x{1F997}\\x{1F998}-\\x{1F9A2}\\x{1F9A3}-\\x{1F9A4}\\x{1F9A5}-\\x{1F9AA}\\x{1F9AB}-\\x{1F9AD}\\x{1F9AE}-\\x{1F9AF}\\x{1F9B0}-\\x{1F9B9}\\x{1F9BA}-\\x{1F9BF}\\x{1F9C0}\\x{1F9C1}-\\x{1F9C2}\\x{1F9C3}-\\x{1F9CA}\\x{1F9CB}\\x{1F9CC}\\x{1F9CD}-\\x{1F9CF}\\x{1F9D0}-\\x{1F9E6}\\x{1F9E7}-\\x{1F9FF}" +
                "\\x{1FA70}-\\x{1FA73}\\x{1FA74}\\x{1FA75}-\\x{1FA77}\\x{1FA78}-\\x{1FA7A}\\x{1FA7B}-\\x{1FA7C}\\x{1FA80}-\\x{1FA82}\\x{1FA83}-\\x{1FA86}\\x{1FA87}-\\x{1FA88}\\x{1FA90}-\\x{1FA95}\\x{1FA96}-\\x{1FAA8}\\x{1FAA9}-\\x{1FAAC}\\x{1FAAD}-\\x{1FAAF}\\x{1FAB0}-\\x{1FAB6}\\x{1FAB7}-\\x{1FABA}\\x{1FABB}-\\x{1FABD}\\x{1FABF}\\x{1FAC0}-\\x{1FAC2}\\x{1FAC3}-\\x{1FAC5}\\x{1FACE}-\\x{1FACF}\\x{1FAD0}-\\x{1FAD6}\\x{1FAD7}-\\x{1FAD9}\\x{1FADA}-\\x{1FADB}\\x{1FAE0}-\\x{1FAE7}\\x{1FAE8}\\x{1FAF0}-\\x{1FAF6}\\x{1FAF7}-\\x{1FAF8}" +
                "\\x{231A}-\\x{231B}\\x{23E9}-\\x{23EC}\\x{23F0}\\x{23F3}" +
                "\\x{25FD}-\\x{25FE}" +
                "\\x{2614}-\\x{2615}\\x{2648}-\\x{2653}\\x{267F}\\x{2693}\\x{26A1}\\x{26AA}-\\x{26AB}\\x{26BD}-\\x{26BE}\\x{26C4}-\\x{26C5}\\x{26CE}\\x{26D4}\\x{26EA}\\x{26F2}-\\x{26F3}\\x{26F5}\\x{26FA}\\x{26FD}" +
                "\\x{2705}\\x{270A}-\\x{270B}\\x{2728}\\x{274C}\\x{274E}\\x{2753}-\\x{2755}\\x{2757}\\x{2795}-\\x{2797}\\x{27B0}\\x{27BF}" +
                "\\x{2B1B}-\\x{2B1C}\\x{2B50}\\x{2B55}]";
        private static final String BASIC_EMOJI_COMBINED_UNICODE_PATTERN = "[\\x{00A9}\\x{00AE}" +
                "\\x{1F170}\\x{1F171}\\x{1F17E}\\x{1F17F}" +
                "\\x{1F202}\\x{1F237}" +
                "\\x{1F321}\\x{1F324}\\x{1F325}\\x{1F326}\\x{1F327}\\x{1F328}\\x{1F329}\\x{1F32A}\\x{1F32B}\\x{1F32C}\\x{1F336}\\x{1F37D}\\x{1F396}\\x{1F397}\\x{1F399}\\x{1F39A}\\x{1F39B}\\x{1F39E}\\x{1F39F}\\x{1F3CB}\\x{1F3CC}\\x{1F3CD}\\x{1F3CE}\\x{1F3D4}\\x{1F3D5}\\x{1F3D6}\\x{1F3D7}\\x{1F3D8}\\x{1F3D9}\\x{1F3DA}\\x{1F3DB}\\x{1F3DC}\\x{1F3DD}\\x{1F3DE}\\x{1F3DF}\\x{1F3F3}\\x{1F3F5}\\x{1F3F7}" +
                "\\x{1F43F}\\x{1F441}\\x{1F4FD}" +
                "\\x{1F549}\\x{1F54A}\\x{1F56F}\\x{1F570}\\x{1F573}\\x{1F574}\\x{1F575}\\x{1F576}\\x{1F577}\\x{1F578}\\x{1F579}\\x{1F587}\\x{1F58A}\\x{1F58B}\\x{1F58C}\\x{1F58D}\\x{1F590}\\x{1F5A5}\\x{1F5A8}\\x{1F5B1}\\x{1F5B2}\\x{1F5BC}\\x{1F5C2}\\x{1F5C3}\\x{1F5C4}\\x{1F5D1}\\x{1F5D2}\\x{1F5D3}\\x{1F5DC}\\x{1F5DD}\\x{1F5DE}\\x{1F5E1}\\x{1F5E3}\\x{1F5E8}\\x{1F5EF}\\x{1F5F3}\\x{1F5FA}" +
                "\\x{1F6CB}\\x{1F6CD}\\x{1F6CE}\\x{1F6CF}\\x{1F6E0}\\x{1F6E1}\\x{1F6E2}\\x{1F6E3}\\x{1F6E4}\\x{1F6E5}\\x{1F6E9}\\x{1F6F0}\\x{1F6F3}" +
                "\\x{203C}\\x{2049}" +
                "\\x{2122}\\x{2139}\\x{2194}\\x{2195}\\x{2196}\\x{2197}\\x{2198}\\x{2199}\\x{21A9}\\x{21AA}" +
                "\\x{2328}\\x{23CF}\\x{23ED}\\x{23EE}\\x{23EF}\\x{23F1}\\x{23F2}\\x{23F8}\\x{23F9}\\x{23FA}" +
                "\\x{24C2}" +
                "\\x{25AA}\\x{25AB}\\x{25B6}\\x{25C0}\\x{25FB}\\x{25FC}" +
                "\\x{2600}\\x{2601}\\x{2602}\\x{2603}\\x{2604}\\x{260E}\\x{2611}\\x{2618}\\x{261D}\\x{2620}\\x{2622}\\x{2623}\\x{2626}\\x{262A}\\x{262E}\\x{262F}\\x{2638}\\x{2639}\\x{263A}\\x{2640}\\x{2642}\\x{265F}\\x{2660}\\x{2663}\\x{2665}\\x{2666}\\x{2668}\\x{267B}\\x{267E}\\x{2692}\\x{2694}\\x{2695}\\x{2696}\\x{2697}\\x{2699}\\x{269B}\\x{269C}\\x{26A0}\\x{26A7}\\x{26B0}\\x{26B1}\\x{26C8}\\x{26CF}\\x{26D1}\\x{26D3}\\x{26E9}\\x{26F0}\\x{26F1}\\x{26F4}\\x{26F7}\\x{26F8}\\x{26F9}" +
                "\\x{2702}\\x{2708}\\x{2709}\\x{270C}\\x{270D}\\x{270F}\\x{2712}\\x{2714}\\x{2716}\\x{271D}\\x{2721}\\x{2733}\\x{2734}\\x{2744}\\x{2747}\\x{2763}\\x{2764}\\x{27A1}" +
                "\\x{2934}\\x{2935}" +
                "\\x{2B05}\\x{2B06}\\x{2B07}" +
                "\\x{3030}\\x{303D}" +
                "\\x{3297}\\x{3299}" +
                "]\\x{FE0F}";
        private static final String EMOJI_PATTERN = "["
                + BASIC_EMOJI_SINGLE_UNICODE_PATTERN + "|"+ BASIC_EMOJI_COMBINED_UNICODE_PATTERN // Basic_Emoji
                + "[\\x{1F1E6}-\\x{1F1FF}][\\x{1F1E8}\\x{1F1FC}]" // RGI_Emoji_Flag
                + "\\x{1F3F4}\\x{E0067}\\x{E0062}[\\x{E0063}-\\x{E0077}]\\x{E007F}" // RGI_Emoji_Tag
                + "[\\x{1F385}-\\x{1FAF8}\\x{261D}-\\x{270D}][\\x{1F3FB}-\\x{1F3FF}]" +// RGI_Emoji_Modifier
                "]+"
                + "|[0-9#*]\\x{FE0F}\\x{20E3}"; // Keycap_Emoji
    
        public static String removeEmoji(String str) {
            return str.replaceAll(EMOJI_PATTERN, "");
        }
    }

    아래와 같이 아주아주 고도화된 테스트도 잘 통과한다.

    이모지 제거 테스트 성공

    그리고 혹시 또 있을 예외 케이스가 있을지 몰라 이모지 전체(중간에 범위로 표시된 애들은 몇개 생략)를 다운 받아 전수 조사를 하였는데 모두 통과하였다.

    @Test
        @DisplayName("모든 이모지에 대해 제거 여부를 테스트한다.")
        public void allEmojiCheck() throws IOException {
            BufferedReader reader = new BufferedReader(new FileReader("/Users/juhyun.oh/Desktop/emoji-sequences.txt"));
            String str;
            List<String> allEmojis = new ArrayList<>();
            try {
                while ((str = reader.readLine()) != null) {
                    if (str.startsWith("#") || str.length() == 0) {
                        continue;
                    }
                    String[] split = str.split("]");
                    List<String> emojis = Arrays.stream(split[1]
                            .replaceAll(" ", "")
                            .replaceAll("\\(", "")
                            .replaceAll("\\)", "")
                            .split("\\.\\."))
                            .collect(Collectors.toList());
                    allEmojis.addAll(emojis);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            allEmojis.forEach(
                    emoji -> {
                        System.out.println(emoji);
                        assertThat(EmojiUtil.removeEmoji(emoji)).isEqualTo("");
                    }
            );
            reader.close();
        }

    하지만 저 정규식에는 3가지 문제가 있다고 생각한다.

    1. 복잡하다. 나같은 정규식 까막눈이든 다른 사람이든 저 정규식을 보면 현기증이 날 것이다.
    2. 그러므로 혹시라도 16버전이라든지 추가 이모지 건이 나오면 수정하기가 매우 까다롭다.
    3. 성능도 고려해야 한다. 정규식이 어떤 메커니즘으로 문자열 검사하는지 잘 모르지만 저 조건에 다 만족해야 한다면 비교 연산이 많아지긴 할 것이다.

    다행인지 불행인지 다른 이슈로 인해 저 코드가 나갈 필요가 없어져 제품에는 반영되지 않았다. 

    emoji-java(https://github.com/vdurmont/emoji-java)라는 라이브러리도 있으나 2020년 1월이 마지막 커밋이라 그간의 이모지 업데이트가 반영안되어있고, 심지어 옛날 이모지도 잘 걸러주지 못한다ㅠㅠ

     

    emoji-java 테스트

    근본적으로 이모지를 더 정교하고 세련되게 필터링할 방법이 필요하다. 파이썬 진영 라이브러리를 보니까 사전으로 관리하고 있던데, 사이드 프로젝트로 자바 진영쪽 라이브러리나 만들어볼까? 뭔가 재미있을 것 같긴하다. 언젠간 쓸모 있겠지...

     

    도움받은 글


    https://meetup.nhncloud.com/posts/317

     

    Java에서의 Emoji처리에 대해 : NHN Cloud Meetup

    Java에서의 Emoji처리에 대해

    meetup.nhncloud.com

    https://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C_%ED%8F%89%EB%A9%B4

     

    유니코드 평면 - 위키백과, 우리 모두의 백과사전

    위키백과, 우리 모두의 백과사전. 유니코드 평면은 유니코드 전체를 논리적으로 나눈 구획을 말한다. 0번(다국어 기본 평면)에서부터 16번까지 모두 17개로 나뉘며, 각 평면은 65536개(216개)의 코드

    ko.wikipedia.org

     

    댓글

Designed by Tistory.