NOMO.asia

배경

트위치(Twitch.tv)의 채팅창을 비롯한 많은 채팅 시스템은 각 유저의 닉네임에 색깔을 입혀서 구별을 쉽게할 수 있도록 기능을 제공한다. 트위치의 경우, 채팅창에서의 자신의 닉네임 색상은 유저 자신이 직접 지정할 수도 있지만, 첫 접속 시에는 랜덤으로 지정되는 것으로 알고있다.


참고로 트위치에서 나의 채팅 닉네임 색상을 설정하는 방법은, 채팅 입력한 근처의 [톱니바퀴 모양 아이콘]을 누르고 [표시 방법 편집] 을 누르면 [이름 색상]을 설정할 수 있다.



이렇게 초기에 닉네임 색상을 지정하는 것을 server 단에서 컨트롤한다면
  1. 어떤 유저가 채팅에 처음 접속할 때
  2. 사전에 정해둔 색상 배열에서 랜덤으로 색깔을 하나 골라
  3. 유저에게 지정하고
  4. 지정된 색깔을 서버에 저장해두고 활용하면 된다.

따라서 이 경우 색깔을 지정하는 방법을 별로 고민할 필요가 없다.


색상은 rgb 값을 랜덤으로 만들어낼 수도 있지만 일반적으로 사전에 정해진 몇 개의 색상에서 랜덤으로 가져오는데, 이유는 배경색에 따라 잘 보이는 색상이 정해져있기 때문이다. 물론 rgb 값 생성 시 범위 지정이나 밝음/어두움에 대한 평가가 가능하므로, 랜덤으로 색상을 생성하더라도 어떻게든 잘 보이게 만들어내는 것이 가능하긴 하다. 하지만 색상이 너무 많을 경우 난잡해보일 수도 있고, 정해진 리스트에서 색상을 가져다 쓰는게 그냥 간편하기 때문에 주로 이 방법이 사용되는 것같다.

내가 하고 싶었던 것

그런데 내가 하고 싶었던 것은, 어떤 채팅 시스템이 무작위 패턴의 익명 닉네임(무작위이지만 각 유저의 닉네임은 고유함)을 사용하고 있고, 닉네임을 색상으로 구분하지도 않을 때, 임의로 닉네임마다 랜덤 색상을 부여해서 유저 구분을 더 확실히 할 수 있는 브라우저 확장기능을 만드는 것이었다.


익명으로 채팅창을 유지하려는 운영자의 의도와는 다를 수 있지만, 특정 유저를 구분해서 기억하고자 한다기 보다는 채팅 흐름을 좀 더 쉽게 읽기 위한 의도가 더 컸다.

목표

즉 내가 원하는 것은 이렇다.

  • 어떠한 텍스트(내 경우에는 닉네임)가 주어졌을 때 랜덤으로 색상이 결정되어야 하고,
  • 주어진 텍스트에 따라 색상은 달라야 하지만,
  • 동일한 텍스트에 의한 색상은 매번 같아야 한다.

동작 방법 구상

유저 정보를 클라이언트에 저장하는 경우의 동작 순서

만약 유저의 닉네임을 비롯한 유저 정보를 클라이언트에 저장하기로 한다면 구현은 좀 더 쉬워질 것이다.
  1. [유저 닉네임-색상] 리스트를 하나 만들어 클라이언트에 저장한다.
    Cookie, Local Storage 나 확장기능에서 제공하는 저장공간을 이용할 수 있다.
  2. 어떤 유저가 채팅을 쳤을 때, 리스트에 포함된 닉네임인지 확인한다.
  3. 만약 리스트에 포함되어 있지 않은 닉네임이라면
    사전에 정해둔 색상 배열에서 랜덤으로 색깔을 하나 골라 유저에게 지정하고 저장한다.
  4. 만약 리스트에 포함된 닉네임이라면 저장된 색상을 가져온다.
  5. 결정된 색상을 이용해 닉네임을 노출한다.

유저 정보를 클라이언트에 저장하지 않는 경우의 동작 순서

그렇지만 만약 유저 정보를 클라이언트에 저장하지 않기로 한다면, 아래와 같이 해야한다.

  1. 유저의 닉네임을 고유한 숫자값으로 변환한다.
  2. 해당 숫자값을 이용하여 사전에 정해둔 색상 배열에서 랜덤으로 색깔을 하나 골라
  3. 결정된 색상을 이용해 닉네임을 노출한다.


내 경우에는 대상 채팅 시스템의 사용자가 1000명~10000명 가까이 되기도 하고, 사용자가 유저 정보를 저장하게 하고 싶지도 않아서 후자의 방법을 사용하기로 했다.

코드

자세한 알고리즘은 생략하고 코드로 대체한다.
// Javascript implementation of Java’s String.hashCode() method
// String to 32bit integer
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
String.prototype.hashCode = function () {
    var hash = 0,
        i, char;
    if (this.length === 0) return hash;
    for (i = 0; i < this.length; i++) {
        char = this.charCodeAt(i);
        hash = ((hash << 5) - hash) + char;
        hash & hash; // Convert to 32bit integer
    }
    return hash;
};

var random_color = {
    lib: {
        orange:"#FF4500",brownishorange:"#DAA520",darkgreen:"#008000",blue:"#0000FF",blueviolet:"#8a2be2",brown:"#a52a2a",cadetblue:"#5f9ea0",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",crimson:"#dc143c",darkblue:"#00008b",darkgoldenrod:"#b8860b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",dimgray:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",forestgreen:"#228b22",grey:"#808080",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",lightcoral:"#f08080",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightslategrey:"#778899",limegreen:"#32cd32",magenta:"magenta",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",navy:"#000080",olive:"olive",olivedrab:"#6b8e23",orangered:"#ff4500",orchid:"#da70d6",pink:"#FF69B4",purple:"purple",red:"#FF0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",seagreen:"#2e8b57",sienna:"#a0522d",slateblue:"#6a5acd",slategrey:"#708090",steelblue:"#4682b4",tan:"#d2b48c",tomato:"#ff6347",violet:"#ee82ee",
    },
    random: function (str) {
        var hash, color_key;
        var colors_keys = Object.keys(this.lib);
        var colors_keys_length = colors_keys.length;

        // 입력 값이 없는 경우 임의의 랜덤 색상값 출력
        if (str === undefined) {
            hash = Math.floor((Math.random() * colors_keys_length) + 1); // random range: 0 - colors_keys_length
        }
        // 입력 값이 있는 경우, String 의 hash 에 따른 색상값 출력
        else {
            hash = str.hashCode();
            hash = ((hash % colors_keys_length) + colors_keys_length) % colors_keys_length; // range: 0 - colors_keys_length
        }

        color_key = colors_keys[hash];
        return { name: color_key, rgb: this.lib[color_key] };
    }
};

lib 라는 객체에 색상을 저장해놨는데, 흰색 배경에서 잘 보이는 글자색을 내가 임의로 고른 것이다. 만약 이 객체에 색상을 추가하거나 제거하여 배열 길이가 바뀌는 경우, 같은 텍스트에 대해 변경 이전과 이후의 색깔이 달라질 수 있다.


텍스트의 고유한 숫자값, 그러니까 해쉬값을 계산하는 방법은 내가 가져다 쓴 방법 외에도 아주 많았다. 내가 가져다 쓴 방법의 경우 << 와 & 와 같은 비트연산자를 사용하는데, 비트연산자를 사용하지 않고도 동일한 계산이 가능하지만 비트연산자를 이용한 계산이 더 빠르다고 한다. 최종적으로 32bit 의 integer 값이 계산된다. 비트 연산자에 대한 설명은 이곳을 참고.


이렇게 계산한 해쉬값의 경우 마이너스 값을 가질 수도 있기 때문에, 해쉬값 계산 후 이 값을 전체 색상의 개수로 나눠 나머지를 가져올 때
그냥 hash % colors_keys_length 와 같이 계산하면 이 값이 마이너스 값을 가질 수도 있기 때문에 주의해야 한다.


채팅방 접속 인원보다 실제 채팅을 하는 인원은 상대적으로 소수이므로, 계산된 색상값을 javascript 변수 값에 일정 개수만큼만 저장해둬서 계산을 줄일 수도 있을 것 같은데, 배열에 저장하고 저장된 값을 찾아다 쓰는것과 매번 계산하는 것이 얼마만큼의 차이를 보일지는 모르겠다. 애초에 의미없는 수준일듯.


사용 예제는 아래와 같다. 버튼을 클릭하면 각 element의 html() 값을 이용하여 색상을 지정하도록 했다. 동일한 내용을 가지는 경우 항상 동일한 색상으로 표현되는 것을 볼 수 있다. 링크는 https://jsfiddle.net/nomomo/9j21h7ba/