Polyfill

홈페이지 : https://polyfill.io


- 특정 기능이 지원되지 않은 브라우저를 위해 사용할 수 있는 코드 조각이나 플러그인을 말함. Polyfill은 HTML5 및 CSS3를 사용하는데 있어 오래된 브라우저 사이의 간격을 메꾸는 역할을 함

자동으로 로드되는 polyfill : https://cdn.polyfill.io/v2/docs/
        

아래처럼 polyfill 을 로드하면 브라우저에 맞는 스크립트가 출력된다. 크롬이나 파이어폭스가 로딩하면 코드는 거의 없고 IE에서 로드하면 코드가 많이 나온다.

<!-- 브라우저 환경에 맞게 자동으로 스크립트 로딩 -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>

<!-- 브라우저에 상관없이 전체 소스 로딩 -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=default|always"></script>

<!-- Promise 만 로딩 -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise|always"></script>



HTML5 Cross Browser Polyfills
Modernizr 측에서 정리하여 제공하고 있는 각종 폴리필 목록을 참고하면 원하는 기술을 선택하는데 도움이 될 것이다.

html5shiv : HTML5의 섹셔닝 요소(예: <header>, <nav>)를 지원하지 않는 브라우저를 위한 라이브러리
mediaelement.js : HTML5의 <video>와 <audio> 요소들을 모든 브라우저에서 하나의 파일로 같은 UI를 제공하기 위한 라이브러리
Placeholder : HTML5의 플레이스홀더(placeholder)의 지원을 통일하기 위해서 제공하는 라이브러리
h5Validate : HTML5의 폼검증 기능을 구현하기 위해 제공하는 라이브러리
selectivizr : CSS3의 의사 클래스(pseudo-class)와 속성 선택자들을 IE6~8에서 지원하기 위한 라이브러리
css3pie : CSS3의 border-radius와 box-shadow, liner-gradient를 IE6~9 브라우저에서 지원하기 위한 라이브러리
Respond : 반응형 웹의 필수 속성중에 하나인 미디어쿼리를 IE6~8에서 지원하기 위한 라이브러리

더 많은 polyfill : https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills



Modernizr
- 사용자의 브라우저가 현재 가지고 있는 HTML5, CSS 기능들을 감지하고 지원여부를 판별하는 Javascript 라이브러리 임. 다양한 브라우저에서 지원되는 기능을 하나씩 확인해가면서 개발하는 것은 현실적으로 불가능하기 때문에 Modernizr와 같은 라이브러리를 통해 필요기능을 감지하고 지원 여부에 따라 개발자가 동적으로 처리를 달리할 수 있음

홈페이지 : https://modernizr.com/
Github : https://github.com/Modernizr/Modernizr


========================================================================================================

Object doesn't support property or method 'assign'

IE Object.assign 오류 시 polyfill

if (!Object.assign) {
Object.defineProperty(Object, 'assign', {
            enumerable: false,
    configurable: true,
    writable: true,
    value: function(target) {
    'use strict';
    if (target === undefined || target === null) {
    throw new TypeError('Cannot convert first argument to object');
    }
    var to = Object(target);
    for (var i = 1; i < arguments.length; i++) {
    var nextSource = arguments[i];
    if (nextSource === undefined || nextSource === null) {
    continue;
    }
    nextSource = Object(nextSource);
    var keysArray = Object.keys(Object(nextSource));
    for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
    var nextKey = keysArray[nextIndex];
    var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
    if (desc !== undefined && desc.enumerable) {
    to[nextKey] = nextSource[nextKey];
    }
    }
    }
    return to;
    }
});
}


출처 : http://webframeworks.kr/tutorials/translate/explanation-of-this-in-javascript-1/

참고 : https://hyunseob.github.io/2016/03/10/javascript-this/

1. this에 대한 미스터리

많은 시간 동안 this 키워드는 자바스크립트 개발자들에게 미스터리의 대상이었다. this는 강력한 기능임에도 불구하고 쉽게 이해하기 힘든 부분이다.

Java, PHP와 같은 언어에서 this는 클래스로부터 생성되는 인스턴스 중 현재 객체를 의미한다. 그 이상 그 이하도 아니다. 대부분 클래스 밖에서는 사용될 수 없으며 이러한 접근 방법으로는 혼란이 생기지 않는다.

자바스크립트에서 this는 함수의 현재 실행 문맥이다. 자바스크립트에는 4가지의 함수 실행 타입이 있기 때문이다.

  • 함수 실행: alert('Hello World!')
  • 메소드 실행: console.log('Hello World!')
  • 생성자 실행: new RegExp('\d')
  • 간접 실행: alert.call(undefined, 'Hello World!')

각각의 타입은 서로 다른 각각의 문맥을 가진다. 이 부분에서 개발자들의 예상과 다른 부분이 생긴다.

더욱이 엄격 모드 역시 실행 문맥에 영향을 미친다.

this 키워드를 이해할 수 있는 방법은 함수 실행, 함수 실행이 문맥에 어떤 영향을 미치는지에 대해 확실히 이해하는 것이다.

이 글은 실행에 대한 설명에 초점을 맞췄다. 그리고 함수 호출이 this에 어떤 영향을 미치는지, 그리고 문맥을 식별하는 과정에서 저지르는 일반적인 실수들을 보여줄 것이다.

시작하기 전에 아래의 용어들과 친숙해지자.

  • 실행이란, 함수 내부에 있는 코드를 수행하는 것이다. (쉽게 말해 함수 호출이라고 보면 됨) 예를 들어 parseInt라는 함수는 parseInt('15', 10)로 실행된다.
  • 실행 시점에서의 문맥은 함수 내부에서의 값이다.
  • 함수의 스코프는 변수, 객체, 내부 함수들의 집합이다.

2. 함수 실행

함수 실행은 함수 객체로 계산 될 표현식이 열림 괄호, 콤마로 구분되는 인자들, 그리고 닫힘 괄호와 함께 수행된다. parseInt('18')가 함수 실행의 예제다.

이 표현식은 myObject.myFunction와 같이 메소드를 실행하기 위해 사용하는 속성 접근자가 될 수 없다. 예를 들어 [1,5].join(',')는 함수 실행이 아니라 메소드 호출이다.

함수 실행에 대한 간단한 예제

function hello(name) {
  return 'Hello ' + name + '!';
}
// 함수 실행
var message = hello('World');
console.log(message); // => 'Hello World!'

hello('World')은 함수 실행이다. hello표현식은 뒤따라 오는 'World' 인자와 함께 함수 객체로 계산 될 것이다.

A more advanced example is the IIFE (immediately-invoked function expression): 심화 예제로 IIFE(즉시실행함수)가 있다.

var message = (function(name) {
   return 'Hello ' + name + '!';
})('World');
console.log(message) // => 'Hello World!'

즉시실행함수 역시 함수 실행의 종류 중 하나다. 앞 부분에 위치한 괄호 한 쌍은 함수 객체로 바뀔 표현식이고, 뒤에 위치한 괄호 한 쌍은 함수에 전달될 인자들이다. 여기에서는 'World'가 인자다.

2.1 함수 실행에서의 this

함수 실행에서의 this는 전역 객체다.

전역 객체는 실행 환경에 따라 결정된다. 웹 브라우저에서는 window 객체가 전역 객체다.

함수 실행에서의 실행 문맥은 전역 객체다.

아래의 함수 예제를 통해 문맥을 체크해보자.

function sum(a, b) {
   console.log(this === window); // => true
   this.myNumber = 20; // 전역 객체에 'myNumber'라는 속성을 추가
   return a + b;
}
// sum()은 함수 호출이다.
// sum()에서의 this는 전역 객체다. (window)
sum(15, 16);     // => 31
window.myNumber; // => 20

sum(15, 16)이 실행되자마자, 자바스크립트는 자동으로 this를 전역 객체로 가진다. 웹 브라우저 환경에서의 this는 window다.

this가 함수 스코프 밖(최상단: 전역 실행 문맥)에서 사용되었을 경우, 여기서의 this 역시 전역 객체를 참조하게 된다.

console.log(this === window); // => true
this.myString = 'Hello World!';
console.log(window.myString); // => 'Hello World!'
<!-- In an html file -->
<script type="text/javascript">
   console.log(this === window); // => true
</script>

2.2 엄격 모드에서 함수 실행에서의 this

엄격 모드에서 함수 실행에서의 this는 undefined다.

엄격 모드는 코드 안정성과 더 나은 오류 검증을 제공하기 위해 ECMA Script 5.1 버전에서 처음 소개 되었다. 엄격 모드로 작성하기 위해 함수 내부의 최상단에 'use strict'라는 예약어를 적는다. 이 모드는 실행 문맥인 this를 undefined로 만든다. 실행 문맥은 더이상 전역 객체로 되지 않고, 위의 2.1 케이스와 반대의 상황이 된다.

엄격 모드로 실행되는 예제 코드

function multiply(a, b) {
  'use strict'; // 엄격 모드
  console.log(this === undefined); // => true
  return a * b;
}
// multiply() 함수는 엄격 모드로 실행됨
// multiply()에서의 this는 undefined
multiply(2, 5); // => 10

multiply(2, 5) 함수가 실행될 때 this는 undefined다.

엄격 모드는 현재 스코프 뿐만 아니라 내부 스코프에서도 적용된다. (내부에 정의된 모든 함수에 적용됨)

function execute() {
   'use strict'; // 엄격 모드
   function concat(str1, str2) {
     // 이곳에서도 마찬가지로 엄격 모드
     console.log(this === undefined); // => true
     return str1 + str2;
   }
   // concat() 함수는 엄격 모드
   // concat() 함수 안에서의 this는 undefined
   concat('Hello', ' World!'); // => "Hello World!"
}
execute();

'use strict'은 최상단에 위치하고, 해당 스코프에 엄격 모드를 실행해준다. concat은 실행 스코프 내에 정의되어있기 때문에 엄격 모드를 상속 받는다. 그리고 concat('Hello', ' World!') 실행은 this를 undefined로 만든다.

자바스크립트 파일에는 엄격 모드, 비 엄격 모드 두 가지 모두 포함되어있다. 그래서 같은 실행 타입에서 서로 다른 모드를 적용할 수 있다.

function nonStrictSum(a, b) {
  // 비 엄격 모드
  console.log(this === window); // => true
  return a + b;
}
function strictSum(a, b) {
  'use strict';
  // 엄격 모드
  console.log(this === undefined); // => true
  return a + b;
}
// nonStrictSum() 함수는 비 엄격 모드로 실행
// nonStrictSum()에서의 this는 window 객체
nonStrictSum(5, 6); // => 11
// strictSum() 함수는 엄격 모드로 실행
// strictSum()에서의 this는 undefined
strictSum(8, 12); // => 20

2.3. 실수: 내부 함수에서의 this를 사용할 때

함수를 실행할 때 흔히 하는 실수가 외부 함수에서의 this와 내부 함수에서의 this를 동일하게 생각하는 것이다.

사실 내부 함수의 문맥은 외부 함수의 문맥에 의존되는 게 아니라 오직 실행 환경에 좌우된다. 기대하는 되로 this가 동작되려면 수정이 필요하다. (call이라 apply 메소드를 사용하는 간접 실행, 또는 바인딩 함수를 적용)

아래의 예제는 두 수의 합계를 계산해준다.

var numbers = {
   numberA: 5,
   numberB: 10,
   sum: function() {
     console.log(this === numbers); // => true
     function calculate() {
       // this는 window, 엄격 모드였으면 undefined
       console.log(this === numbers); // => false
       return this.numberA + this.numberB;
     }
     return calculate();
   }
};
numbers.sum(); // NaN, 엄격 모드였으면 TypeError

numbers.sum()은 객체 내에 있는 메소드를 실행하는 것이다. 그래서 sum 메소드 내의 문맥은 numbers 객체다. calculate 함수는 sum 내부에 정의되어있다. 그래서 아마도 calculate() 역시 this를 numbers 객체로 바라보고 있을 거라고 예상한다. 하지만 calculate()은 메소드 실행이 아닌 함수 실행이다. 그리고 이 함수에서의 this는 전역 객체인 window다. (만약, 엄격 모드였다면 undefined) 비록 외부 함수의 문맥이 numbers 객체지만, calculate 함수에는 영향을 미치지 않는다.

numbers.sum()의 실행 결과는 NaN, 혹은 엄격 모드에서 numberA 속성이 undefined이므로 접근할 수 없어서 TypeError다. calculate 함수는 제대로 실행되지 않았기 때문에 실행 결과는 기대한대로 5 + 10 = 15가 되지 않는다.

이 문제를 해결하기 위해, calculate 함수 역시 sum method와 동일한 문맥으로 되어야 한다. 그래야 numberA와 numberB 속성에 접근할 수 있기 때문이다. 해결책 중 하나로 .call 메소드를 사용하는 것이다.

var numbers = {
   numberA: 5,
   numberB: 10,
   sum: function() {
     console.log(this === numbers); // => true
     function calculate() {
       console.log(this === numbers); // => true
       return this.numberA + this.numberB;
     }
     // 문맥을 수정하기 위해 .call() 메소드를 적용
     return calculate.call(this);
   }
};
numbers.sum(); // => 15

calculate.call(this)는 이전과 동일하게 계산된다. 하지만 추가로, 첫 번째 파라미터로 들어온 인자로 실행 문맥을 수정해준다. 이제 this.numberA + this.numberB의 결과는 numbers.numberA + numbers.numberB와 동일하게 된다. 그래서 결과값도 예상했던대로 5 + 10 = 15가 된다.

3. 메소드 실행

메소드는 객체의 속성으로 있는 함수다. 예를 들어

var myObject = {
  // helloFunction is a method
  helloFunction: function() {
    return 'Hello World!';
  }
};
var message = myObject.helloFunction();

helloFunction is a method in myObject. To get the method, use a property accessor: myObject.helloFunction. 여기서 helloFunction는 myOjbect의 메소드다. 메소드에 접근하기 위해서는 myObject.helloFunction와 같은 속성 접근자를 이용하면 된다.

메소드 실행은 속성 접근자 형태의 표현식이 함수 객체로 계산되면서 실행된다. 이 표현식은 함수와 마찬가지로 열림 괄호, 함수에 전달할 인자, 닫힘 괄호의 구조다. 이전 예제를 활용하면 myObject.helloFunction()은 myObject라는 객체의 helloFunction라는 메소드 실행이다. [1, 2].join(',') 혹은 /\s/.test('beautiful world') 역시 메소드 실행의 종류다.

앞서 설명한 함수 실행과 메소드 실행이 다르다는 점은 중요하다. 왜냐하면 둘은 서로 다른 타입이기 때문이다. 둘의 가장 큰 차이점은 메소드 실행은 속성 접근자를 통해 function (.functionProperty() or 'functionProperty')를 호출한다. 반면에 함수 실행은 속성 접근자를 사용하지 않고, (())와 같이 바로 호출한다

['Hello', 'World'].join(', '); // 메소드 실행
({ ten: function() { return 10; } }).ten(); // 메소드 실행
var obj = {};
obj.myFunction = function() {
  return new Date().toString();
};
obj.myFunction(); // 메소드 실행

var otherFunction = obj.myFunction;
otherFunction();     // 함수 실행
parseFloat('16.60'); // 함수 실행
isNaN(0);            // 함수 실행

3.1 메소드 실행에서의 this

this는 메소드 실행에서 메소드를 소유하고 있는 객체다

객체 내에 있는 메소드를 실행할 때, 여기서의 this는 객체 자신이다.

숫자를 증가하는 메소드를 가진 객체를 하나 만들어보자.

var calc = {
  num: 0,
  increment: function() {
    console.log(this === calc); // => true
    this.num += 1;
    return this.num;
  }
};
// 메소드 실행. 여기서의 this는 calc.
calc.increment(); // => 1
calc.increment(); // => 2

calc.increment() 호출은 increment 함수의 문맥을 calc 객체로 만들어준다. 그래서 this.num 참조로 number 속성을 증가시하는 게 가능하도록 해준다. 자바스크립트 객체는 프로토타입에 있는 메소드를 상속 받는다. 상속 받은 메소드를 객체 내에서 실행한다면 메소드에서의 문맥은 객체 자신을 가리키게 된다.

var myDog = Object.create({
  sayName: function() {
     console.log(this === myDog); // => true
     return this.name;
  }
});
myDog.name = 'Milo';
// 메소드 실행. 여기서의 this는 myDog.
myDog.sayName(); // => 'Milo'

Object.create()는 myDog라는 새로운 객체를 만들고, 프로토타입을 설정한다. myDog 객체는 sayName이라는 메소드를 상속받는다. myDog.sayName()이 실행될 때, myDog가 실행 문맥이다.

ECMAScript 6의 class 예약어에서 메소드 실행 문맥은 위와 마찬가지로 인스턴스 자신을 가리킨다.

class Planet {
  constructor(name) {
    this.name = name;
  }
  getName() {
    console.log(this === earth); // => true
    return this.name;
  }
}
var earth = new Planet('Earth');
// 메소드 실행. 여기서의 this는 earth.
earth.getName(); // => 'Earth'

3.2 실수: 객체로부터 메소드를 분리할 때

객체 내에 있는 메소드는 별도의 변수로 분리할 수 있다. 이 변수를 통해 메소드를 호출할 때, 당신은 아마도 여기서의 this가 메소드가 정의되어있는 객체라고 생각할 것이다.

사실 객체 밖에 있는 메소드를 호출할 경우, 함수 실행을 한 결과와 같다. 함수 실행을 할 경우 this는 전역 객체인 window를 가리킨다. (엄격 모드에서는 undefined) .bind() 바인딩 함수를 사용해서 문맥을 수정할 경우, 메소드를 객체에 포함시킬 수 있다.

아래는 Animal 생성자로 myCat이라는 인스턴스를 생성하는 예제다. 그리고 setTimeout() 함수로 1초 뒤 myCat 객체의 정보를 출력한다.

function Animal(type, legs) {
  this.type = type;
  this.legs = legs;
  this.logInfo = function() {
    console.log(this === myCat); // => false
    console.log('The ' + this.type + ' has ' + this.legs + ' legs');
  }
}
var myCat = new Animal('Cat', 4);
// "The undefined has undefined legs" 출력
// 혹은 엄격모드라면 TypeError 출력
setTimeout(myCat.logInfo, 1000);

아마도 setTimeout으로 myCat.logInfo()를 호출할 때, myCat 객체가 출력될 거라고 예상할 것이다. 하지만 setTimeout의 매개변수로 전달되었기 때문에 메소드는 객체로부터 분리 되어있고, 1초 뒤 함수 실행이 된다. logInfo가 함수로써 실행되기 때문에 여기서의 this는 전역 객체이거나 엄격 모드에서라면 undefined다. 그렇기 때문에 객체의 정보를 기대한 것대로 출력하지 못한다.

함수는 .bind 메소드를 사용해 문맥을 강제로 지정시킬 수 있다. 만약 분리된 메소드가 myCat 객체로 바인딩 된다면 이 문제는 해결된다.

function Animal(type, legs) {
  this.type = type;
  this.legs = legs;
  this.logInfo = function() {
    console.log(this === myCat); // => true
    console.log('The ' + this.type + ' has ' + this.legs + ' legs');
  };
}
var myCat = new Animal('Cat', 4);
// "The Cat has 4 legs" 출력
setTimeout(myCat.logInfo.bind(myCat), 1000);

myCat.logInfo.bind(myCat)는 객체의 메소드가 logInfo라는 새로운 함수로 실행된다. 하지만 바인딩 메소드 덕분에 함수 실행임에도 불구하고, 여기서의 this는 myCat을 가리키게 된다.

4. 생성자 실행

생성자 실행은 표현식 앞에 new라는 키워드가 붙었을 때, 함수 객체로 계산되어 수행된다. 이 표현식은 함수와 마찬가지로 열림 괄호, 함수에 전달할 인자, 닫힘 괄호의 구조다. 예를 들어 new RegExp('\d')와 같다.

아래의 예제는 Country라는 함수가 생성자로 실행되는 내용이다.

function Country(name, traveled) {
   this.name = name ? name : 'United Kingdom';
   this.traveled = Boolean(traveled); // boolean으로 타입 변환
}
Country.prototype.travel = function() {
  this.traveled = true;
};
// Constructor invocation
var france = new Country('France', false);
// Constructor invocation
var unitedKingdom = new Country;

france.travel(); // Travel to France

new Country('France', false)은 Country 함수의 생성자 실행이다. 이것의 실행 결과는 France라는 이름을 가진 새로운 객체다. 만약 생성자에 아무런 매개 변수 없이 실행 된다면, new Country처럼 괄호가 생략되어도 된다.

ECMAScript 6에서는 생성자를 class라는 키워드로 정의할 수 있게 해준다.

class City {
  constructor(name, traveled) {
    this.name = name;
    this.traveled = false;
  }
  travel() {
    this.traveled = true;
  }
}
// 생성자 실행
var paris = new City('Paris', false);
paris.travel();

new City('Paris')은 생성자 실행이다. 이 객체에서 초기 값은 constructor라는 특수 메소드로 설정할 수 있다. 이 메소드 내에서의 this는 새로 만들어지는 객체를 바라보게 된다.

생성자 호출은 생성자의 프로토타입으로부터 속성을 상속받는 새로운 빈 객체를 만든다. 생성자 함수의 역할은 객체를 초기화하는 것이다. 이미 알고 있을지도 모르지만, 이 타입에서 this는 인스턴스를 가리킨다.

myObject.myFunction과 같은 속성 접근자가 new 키워드 뒤에 오게되면, 자바스크립트는 메소드 실행이 아닌 생성자 실행으로 계산한다. 예를 들어 new myObject.myFunction()의 경우, 첫 번째로 extractedFunction = myObject.myFunction과 같이 함수가 추출되고, 그 다음으로 new extractedFunction()와 같이 생성자 실행으로 새로운 객체가 만들어진다.

4.1 생성자 실행에서의 this

생성자 실행에서의 this는 새롭게 만들어진 객체이다.

생성자 실행에서의 문맥은 새롭게 만들어진 객체다. 생성자 실행은 객체에 초기값을 셋팅하기 위해 사용된다. 초기값 셋팅의 예로는 생성자 함수의 매개 변수로 받은 데이터, 속성을 위한 환경 변수, 이벤드 핸들러 등이 있다.

아래 예제를 통해 문맥을 체크해보자.

function Foo () {
  console.log(this instanceof Foo); // => true
  this.property = 'Default Value';
}
// 생성자 실행
var fooInstance = new Foo();
fooInstance.property; // => 'Default Value'

new Foo()은 생성자 실행이다. 여기서의 문맥은 Foo의 인스턴스가 된다. Foo의 내부에서는 초기값이 셋팅되었다. this.property는 default value라는 값을 가진다.

ES6에서 사용 가능한 class 문법 역시 같은 형식이다. 초기값 셋팅은 오직 생성자 메소드에서 할 수 있다.

class Bar {
  constructor() {
    console.log(this instanceof Bar); // => true
    this.property = 'Default Value';
  }
}
// Constructor invocation
var barInstance = new Bar();
barInstance.property; // => 'Default Value'

new Bar()가 실행되면서, 자바스크립트는 생성자 메소드를 통해 문맥을 설정한 빈 객체를 만든다. this.property = 'Default Value'와 같이 this 키워드를 통해 객체에 속성 값을 추가할 수 있다.

4.2 실수: new 깜빡할 때

몇몇 자바스크립트 함수는 생성자 실행 형태로 실행됐을 때 뿐만 아니라 함수 실행으로도 인스턴스를 생성한다. 예를 들어 RegExp가 있다.

var reg1 = new RegExp('\\w+');
var reg2 = RegExp('\\w+');

reg1 instanceof RegExp;      // => true
reg2 instanceof RegExp;      // => true
reg1.source === reg2.source; // => true

new RegExp('\w+')와 RegExp('\w+')가 실행될 때, 자바스크립트는 동일한 정규식 객체를 생성한다.

객체 생성을 위해 함수 실행을 사용하는 것(팩토리 패턴을 제외)은 잠재적 문제를 만들게 된다. 왜냐하면 new 키워드가 생략되었을 때 생성자 함수는 객체를 초기화하는 로직을 생략할지도 모른다.

아래는 해당 문제점이 나타나는 예제다.

function Vehicle(type, wheelsCount) {
  this.type = type;
  this.wheelsCount = wheelsCount;
  return this;
}
// Function invocation
var car = Vehicle('Car', 4);
car.type;       // => 'Car'
car.wheelsCount // => 4
car === window  // => true

Vehicle은 타입과 바퀴개수 속성을 가지는 객체를 만들어주는 함수다.

Vehicle('Car', 4)를 실행하게 되면 객체가 반환된다. 이 객체는 올바른 속성을 가지고 있다. car.type으로 'Car'를, car.wheelCount로 4를 나타낸다. 아마도 초기값을 가진 새로운 객체가 잘 생성되었으리라 예상할 것이다.

하지만 여기서의 this는 함수 실행이 되므로 window 객체를 가리키게 된다. 그리고 Vehicle('Car', 4)은 속성을 window 객체에 추가한다. 잘못된 사용이다. 새로운 객체가 만들어지지 않았다.

생성자를 호출할 때에는 꼭 new 연산자를 사용해야 한다.

function Vehicle(type, wheelsCount) {
  if (!(this instanceof Vehicle)) {
    throw Error('Error: Incorrect invocation');
  }
  this.type = type;
  this.wheelsCount = wheelsCount;
  return this;
}
// 생성자 실행
var car = new Vehicle('Car', 4);
car.type               // => 'Car'
car.wheelsCount        // => 4
car instanceof Vehicle // => true

// 함수 실행. 잘못된 방식.
var brokenCar = Vehicle('Broken Car', 3);

new Vehicle('Car', 4)은 정상 동작한다. 초기 값을 가진 새로운 객체가 생성되었다. 왜냐하면 생성자 실행 앞에 new 키워드를 썼기 때문이다.

검증하는 방법은 생성자 함수에 추가되어 있다. this instanceof Vehicle로 실행 문맥으로 올바른 객체 타입이 맞는지 체크한다. 만약 여기서의 this가 Vehicle이 아니라면 에러가 발생한다. 이와 같은 경우 만약 Vehicle('Broken Car', 3)가 new 키워드 없이 실행된다면, 올바른 실행이 아니라는 에러 메세지를 반환하게 된다.

참고 : http://ujinbot.blogspot.kr/2013/10/blog-post.html


한글 자모 유니코드 : http://www.hipenpal.com/tool/characters-to-unicode-charts-in-korean.php?unicode=94
한글 글자 유니코드 : http://www.hipenpal.com/tool/characters-to-unicode-charts-in-korean.php?unicode=110&nowpage=1

유니코드 변환 테스트 : http://kor.pe.kr/convert.htm

* 이 글은 Mathias Bynens가 쓴 JavaScript has a Unicode problem을 번역한 내용입니다.

자바스크립트가 유니코드를 처리하는 방식은 참... 놀랍다고 할 수 있습니다. 이 글에서는 자바스크립트의 유니코드와 관련된 난점들을 설명하고 흔히 겪는 문제에 대한 해결책을 제시하고자 합니다. 다가오는 ES6가 어떻게 상황을 개선시켰는지에 대해서도 설명합니다.

주의: 이런 말은 정말 하기 싫지만 이 문서는 파이어폭스, 사파리, IE 등 그림문자(emoji)를 렌더링할 수 있는 브라우저에서 보시는 게 좋습니다. OS X의 블링크 브라우저(크롬/오페라)는 이런 글자들을 전혀 렌더링하지 않기 때문에 이 페이지의 코드 예제 일부를 알아보기가 어려울 수 있습니다. 경고했습니다!

유니코드 기본 개념

자바스크립트를 자세히 들여다보기 전에, 먼저 유니코드에 관해 알고 있는 것들을 함께 확실히 해두죠.

유니코드가 뭔지 가장 쉽게 설명하자면, 여러분이 생각할 수 있는 어떤 기호든 코드포인트라 불리는 숫자와 유일한 이름에 매핑시켜주는 데이터베이스라고 할 수 있습니다. 덕분에 특정 기호를, 그 기호를 실제로 사용하지 않고도 쉽게 언급할 수 있는 거죠. 예를 들면

코드포인트는 보통 0을 붙여서 최소 네자리를 가지는 16진수로 표현하고 앞에 U+를 붙입니다.

코드포인트 값은 U+0000에서 U+10FFFF까지 쓸 수 있습니다. 110만 개 이상의 기호를 쓸 수 있는 거죠. 유니코드는 이 코드포인트 범위를 17개의 평면(plane)들로 나누어 정리해놓았습니다. 각각의 평면들은 약 6만 5천자로 이루어집니다.

첫번째 평면은 다국어 기본 평면(Basic Multilingual Plane) 또는 BMP라고 불립니다. 자주 사용되는 글자 대부분이 들어있는 가장 중요한 평면입니다. 대개의 경우 영문 텍스트 문서에 한해서라면 BMP에 들어있지 않은 코드포인트를 쓸 일은 거의 없을 겁니다. 다른 유니코드 평면과 마찬가지로 약 6만 5천자를 묶어놓았습니다.

BMP를 제외하면 약 1백만 자 정도 남았죠. 이 코드포인트가 속한 평면들은 보충 평면들(supplementary planes) 또는 아스트랄 평면들(astral planes)이라고 불립니다.

아스트랄 코드포인트는 쉽게 알아볼 수 있습니다. 코드포인트를 표현하는 16진수가 4자리를 넘어가면 아스트랄 코드포인트입니다.

자 이제 유니코드에 대한 기본적인 이해를 갖췄습니다. 이제 이것이 자바스크립트의 문자열 처리에 어떻게 적용되는지 보겠습니다.

이스케이프 시퀀스

이런 걸 보셨을 거에요.

>> '\x41\x42\x43'
'ABC'
>> '\x61\x62\x63'
'abc'

16진수 이스케이프 시퀀스라고 부릅니다. 대응하는 코드포인트를 가리키는 두 자리 16진수로 이루어져 있죠. 즉 \x41U+0041 라틴 대문자 A를 가리킵니다. 이 이스케이프 시퀀스는 U+0000부터 U+00FF까지의 코드포인트에 사용할 수 있습니다.

다음과 같은 형식의 이스케이프도 많이 사용됩니다.

>> '\u0041\u0042\u0043'
'ABC'
>> 'I \u2661 JavaScript!'
'I ♡ JavaScript!'

이것은 유니코드 이스케이프 시퀀스라고 부릅니다. 4자리의 16진수로 코드포인트를 표현합니다. \u2661U+2661 흰색 하트 수트가 되는 거죠. U+0000부터 U+FFFF까지, 즉 다국어 기본 평면에 들어있는 모든 코드포인트에 사용할 수 있습니다.

하지만 나머지 아스트랄 평면들은 어쩌죠? 이 코드포인트를 가리키려면 16진수가 4자리 이상 필요한데요. 나머지 글자들은 어떻게 이스케이프해야 할까요?

ES6에서는 쉬워요. 유니코드 코드포인트 이스케이프라는 새로운 형식의 이스케이프 시퀀스를 도입했기 때문입니다.

>> '\u{41}\u{42}\u{43}'
'ABC'
>> '\u{1F4A9}'
'💩' // U+1F4A9 개똥

위와 같이 중괄호 안에 16진수를 6자리까지 쓸 수 있기 때문에 모든 유니코드 코드포인트를 사용하는 데 충분합니다. 따라서 이 이스케이프 시퀀스를 쓰면 어떤 유니코드 기호든 코드포인트를 사용해 간단히 이스케이프할 수 있어요.

ES5와 구환경 하위호환시 쓸 수 있는 유감스러운 대책은 대체쌍(surrogate pairs)을 쓰는 것인데요.

>> '\uD83D\uDCA9'
'💩' // U+1F4A9 개똥

이 경우에는 각각의 이스케이프가 코드포인트의 대체물을 반씩 가리키는 거에요. 대체쌍 반쪽 두 개가 하나의 아스트랄 기호를 이루게 됩니다.

대체 코드포인트는 원본 코드포인트랑 전혀 다르게 생겼다는 데 주의하세요. 주어진 아스트랄 코드포인트의 대체쌍을 계산하는 공식이 있어요. 거꾸로 대체쌍을 가지고 아스트랄 코드포인트를 얻어내는 것도 가능하고요.

대체쌍을 사용하면 U+010000부터 U+10FFFF까지 모든 아스트랄 코드포인트를 표현할 수 있습니다...만 BMP 글자를 가리킬 때는 하나의 이스케이프를 사용하고 아스트랄 글자에는 두 개의 이스케이프를 사용한다는 개념 자체가 좀 당황스럽죠. 이것 때문에 짜증나는 일도 많이 생깁니다.

자바스크립트 문자열에서 글자 수 세기

예를 들어 주어진 문자열에서 글자 수를 센다고 합시다. 어떻게 하시겠어요?

일단은 간단하게 length 프로퍼티를 사용하겠죠.

>> 'A'.length // U+0041 라틴 대문자 A
1

>> 'A' == '\u0041'
true

>> 'B'.length // U+0042 라틴 대문자 B
1

>> 'B' == '\u0042'
true

위 예제에서 문자열의 length 프로퍼티는 글자 수를 나타냅니다. 자연스러운 일이죠. 이스케이프 시퀀스를 사용해서 이 글자들을 표현한다면, 글자 하나당 하나씩의 이스케이프가 있으면 됩니다. 하지만 항상 이런 건 아니에요! 약간 다른 예제를 보시죠.

>> '𝐀'.length // U+1D400 수학 볼드체 대문자 A
2

>> '𝐀' == '\uD835\uDC00'
true

>> '𝐁'.length // U+1D401 수학 볼드체 대문자 B
2

>> '𝐁' == '\uD835\uDC01'
true

>> '💩'.length // U+1F4A9 개똥
2

>> '💩' == '\uD83D\uDCA9'
true

내부적으로 자바스크립트는 아스트랄 기호를 대체쌍으로 표현합니다. 그리고 이 대체쌍의 반쪽들을 각각 별개의 '글자'들로 취급해요. ES5-호환 방식의 이스케이프 시퀀스만으로 글자를 표현한다면, 아스트랄 기호 하나마다 두 개의 이스케이프가 필요한 거에요. 이게 엄청 헷갈려요. 사람들은 당연히 유니코드 글자들 기준으로 내지는 자소 단위로 생각하니까요.

아스트랄 기호 계산

다시 질문으로 돌아갑시다. 자바스크립트 문자열의 글자 수를 제대로 세려면 어떻게 해야 할까요? 대체쌍을 적절한 방법으로 계산한 다음, 하나의 쌍을 한 글자로 세면 됩니다. 다음과 같은 방법을 사용할 수 있습니다.

var regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
function countSymbols(string) {
    return string
        // 모든 대체쌍을 BMP 기호로 교체하고
        .replace(regexAstralSymbols, '_')
        // length를 구한다
        .length;
}

Punycode.js를 사용하신다면 (Node.js에 들어있어요) 제공되는 유틸리티 메서드를 사용해 자바스크립트 문자열과 유니코드 코드포인트를 상호 변환할 수 있습니다. punycode.ucs2.decode 메서드는 문자열을 받아 유니코드 코드포인트의 배열을 반환해줍니다. 배열의 요소 하나가 글자 하나에 대응합니다.

function countSymbols(string) {
    return punycode.ucs2.decode(string).length;
}

어느 쪽을 사용하든 이제 코드포인트를 제대로 계산하여 정확한 결과를 얻을 수 있게 되었습니다.

>> countSymbols('A') // U+0041 라틴 대문자 A
1

>> countSymbols('𝐀') // U+1D400 수학 볼드체 대문자 A
1

>> countSymbols('💩') // U+1F4A9 개똥
1

똑같이 생긴 글자 계산

하지만 더 따지고 들면 문자열의 글자 수 세기 문제는 훨씬 더 복잡합니다. 다음 예제를 보세요.

>> 'mañana' == 'mañana'
false

자바스크립트는 두 문자열이 다르다고 말하지만 보기엔 똑같아요! 어떻게 된 일일까요?

제가 만든 자바스크립트 이스케이프 도구의 설명에 따르면 이유는 이렇습니다.

>> 'ma\xF1ana' == 'man\u0303ana'
false

>> 'ma\xF1ana'.length
6

>> 'man\u0303ana'.length
7

첫번째 문자열에는 U+00F1 틸데가 붙은 라틴 소문자 N이 들어있는데, 두번째 문자열에서는 같은 글자를 만들기 위해 두 개의 코드포인트 즉 U+006E 라틴 소문자 NU+0303 틸데 결합자를 사용했습니다. 두 글자가 동일하지 않고 length 값도 다른 것은 이 때문입니다.

어쨌거나 사람과 동일하게 이 문자열에서 글자 수를 센다면 답은 두 문자열 모두 6이 되어야 합니다. 각각의 문자열에서 구별되는 글자가 6개니까요.

ES6에서는 꽤 간단합니다.

function countSymbolsPedantically(string) {
    // 모양이 같은 글자들을 NFC로 정규화한다 (정준 분해한 뒤에 다시 정준 결합)
    var normalized = string.normalize('NFC');
    // 아까처럼 아스트랄 기호/대체쌍을 계산한다
    return punycode.ucs2.decode(normalized).length;
}

String.prototype에 포함된 normalize 메서드가 이러한 차이들을 고려하여 유니코드 정규화를 해줍니다. 하나의 글자를 표현하는 코드포인트 뒤에 결합기호가 붙어있으면 이것을 하나의 코드포인트 형태로 정규화해주는 거죠.

>> countSymbolsPedantically('mañana') // U+00F1
6

>> countSymbolsPedantically('mañana') // U+006E + U+0303
6

ES5와 구 환경에서의 하위 호환을 위해서는 String.prototype.normalize 폴리필을 사용할 수 있습니다.

나머지 결합자 계산

하지만 여전히 완벽하진 않습니다. 코드포인트에 결합자를 여러 개 붙이면, 시각적으로는 항상 한 글자가 되지만 정규화된 형태가 존재하지 않을 수도 있어요. 즉 정규화로 해결할 수 없는 거죠.

>> 'q\u0307\u0323'.normalize('NFC') // q̣̇
'q\u0307\u0323'

>> countSymbolsPedantically('q\u0307\u0323')
3 // 1이어야 함

>> countSymbolsPedantically('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞');
74 // 6이어야 함

보다 정확한 계산이 필요하다면 정규식으로 입력 문자열에서 이런 결합자들을 모두 제거하면 됩니다.

// export-data.js로 생성한 정규식
var regexSymbolWithCombiningMarks = /([\0-\u02FF\u0370-\u1DBF\u1E00-\u20CF\u2100-\uD7FF\uDC00-\uFE1F\uFE30-\uFFFF]|[\uD800-\uDBFF]
[\uDC00-\uDFFF]|[\uD800-\uDBFF])([\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]+)/g; function countSymbolsIgnoringCombiningMarks(string) { // 결합자 기호를 제거하고 원래 글자만 남긴다 var stripped = string.replace(regexSymbolWithCombiningMarks, function($0, symbol, combiningMarks) { return symbol; }); // 아까처럼 아스트랄 기호/대체쌍을 계산한다 return punycode.ucs2.decode(stripped).length; }

이 함수는 결합자는 모두 제외하고 결합자가 속해있는 글자들만 남깁니다. 결합자가 아닌 것들은 건드리지 않습니다. 이 방법은 심지어 ES3 환경에서도 동작합니다. 지금까지 얘기된 것 중 가장 정확한 결과를 제공하죠.

>> countSymbolsIgnoringCombiningMarks('q\u0307\u0323')
1

>> countSymbolsIgnoringCombiningMarks('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞')
6

자바스크립트에서 문자열 뒤집기

비슷한 문제가 또 있어요. 자바스크립트에서 문자열 뒤집기입니다. 이게 뭐 얼마나 어렵겠어요, 그죠? 흔히 쓰이는, 아주 간단한 방법은 다음과 같습니다.

// 소박한 방법
function reverse(string) {
    return string.split('').reverse().join('');
}

대부분의 경우 잘 동작하는 것처럼 보입니다.

>> reverse('abc')
'cba'

>> reverse('mañana') // U+00F1
'anañam'

하지만 결합자나 아스트랄 기호가 포함되면 문자열이 완전 엉망이 돼요.

>> reverse('mañana') // U+006E + U+0303
'anãnam' // 틸데가 n이 아니라 a에 적용됨
>> reverse('💩') // U+1F4A9
'��' // 💩의 대체쌍 순서가 맞지 않음

다행히도 미시 엘리엇이라는 똑똑한 컴퓨터 과학자가 이 문제를 해결할 빈틈 없는 알고리즘을 생각해냈습니다. (랩퍼 미시 엘리엇의 'Work It' 가사임ㅎㅎ)

난 내 꺼 내려놓고, 뒤집고, 거꾸로 하지. 난 내 꺼 내려놓고, 뒤집고, 거꾸로 하지.
(I put my thang down, flip it, and reverse it. I put my thang down, flip it, and reverse it.)

실로 그렇습니다. 문자열을 처리하기 전에 특정 문자에 속한 결합자들의 위치를 모두 맞바꾸고, 대체쌍 순서도 모두 뒤집으면 이 문제를 성공적으로 해결할 수 있습니다. 고마워요, 미시!

// Esrever 사용
>> esrever.reverse('mañana') // U+006E + U+0303
'anañam'
>> esrever.reverse('💩') // U+1F4A9
'💩' // U+1F4A9

문자열 메서드에서 유니코드 관련 문제들

이러한 동작 방식은 문자열의 다른 메서드에도 영향을 미칩니다.

코드포인트를 글자로 변환하기

String.fromCharCode 메서드를 사용해 유니코드 코드포인트로부터 문자열을 얻어낼 수 있는데요. 단 BMP 범위인 U+0000에서 U+FFFF 내의 코드포인트에 대해서만 제대로 동작합니다. 아스트랄 코드포인트에 이 메서드를 사용하면 예상 밖의 결과가 나와요.

>> String.fromCharCode(0x0041) // U+0041
'A' // U+0041
>> String.fromCharCode(0x1F4A9) // U+1F4A9
'' // U+1F4A9이 아니라 마지막 4자리인 U+F4A9이 반환됨

이 문제를 회피하려면 대체쌍 각각의 코드포인트를 직접 계산해서 별개의 인자로 전달하는 방법밖에 없습니다.

>> String.fromCharCode(0xD83D, 0xDCA9)
'💩' // U+1F4A9

대체쌍 반쪽짜리들 계산으로 골치 아프기 싫다면 Punycode.js에 다시 한번 기대봅니다.

>> punycode.ucs2.encode([ 0x1F4A9 ])
'💩' // U+1F4A9

다행히 ES6에는 String.fromCodePoint(codePoint)라는 게 도입되어서 아스트랄 기호들을 제대로 처리해줍니다. 이 메서드는 모든 유니코드 코드포인트, 즉 U+000000부터 U+10FFFF까지 모두 사용할 수 있습니다.

>> String.fromCodePoint(0x1F4A9)
'💩' // U+1F4A9

ES5와 구 환경에서의 하위 호환을 위해서는 String.fromCodePoint() 폴리필을 사용하시고요.

문자열에서 특정 글자 얻기

개똥 글자가 들어있는 문자열에서 String.prototype.charAt(position)을 사용해 첫번째 글자를 얻어내려고 하면, 글자 하나가 아니라 대체쌍 반쪽밖에 가져오지 못합니다.

>> '💩'charAt(0) // U+1F4A9
'\uD83D' // U+D83D, U+1F4A9의 대체쌍 중 첫번째 반쪽

ES6에는 String.prototype.at(position)의 도입이 제안된 상태입니다. charAt과 똑같으면서 필요한 경우에는 대체쌍 반쪽이 아닌 글자 전체를 처리해주는 것이죠.

>> '💩'at(0) // U+1F4A9
'💩' // U+1F4A9

ES5와 구 환경에서의 하위 호환을 위해서는 String.prototype.at() 폴리필을 쓸 수 있습니다

문자열에서 특정 코드포인트 얻기

String.prototype.charCodeAt(position)도 비슷합니다. 아까의 문자열에 이 메서드를 사용해 첫번째 글자의 코드포인트를 가져오려 하면 개똥 글자의 코드포인트가 아닌 그 대체쌍의 첫번째 반쪽에 대한 코드포인트가 반환됩니다.

>> '💩'charCodeAt(0)
0xD83D

다행히 ES6에는 String.prototype.codePointAt(position)가 도입되어서, charCodeAt와 똑같으면서 필요할 때는 대체쌍 반쪽이 아닌 글자 전체를 처리해줍니다.

>> '💩'codePointAt(0)
0x1F4A9

ES5와 구 환경에서의 하위 호환을 위해서는 String.prototype.codePointAt() 폴리필이 있습니다.

문자열 내 모든 글자 순회

문자열 내 모든 글자를 순회하면서 각각의 글자에 대해 어떤 작업을 하고 싶다고 해봅시다.

ES5에서는 대체쌍을 계산하기 위해 상당량의 상용코드(boilerplate code)를 작성해야만 합니다.

function getSymbols(string) {
    var length = string.length;
    var index = -1;
    var output = [];
    var character;
    var charCode;
    while (++index < length) {
          character = string.charAt(index);
          charCode = character.charCodeAt(0);
          if (charCode >= 0xD800 && charCode <= 0xDBFF) {
              // 주의: 여기서 대체쌍 반쪽자리 하나만 존재하는 경우는 계산하지 못함
              output.push(character + string.charAt(++index));
          } else {
              output.push(character);
          }
    }
    return output;
}

var symbols = getSymbols('💩');
symbols.forEach(function(symbol) {
    assert(symbol == '💩');
});

ES6에서는 간단히 for … of문만 쓰면 됩니다. 문자열 반복문이 대체쌍 대신 온전한 글자를 처리해줍니다.

for (let symbol of '💩') {
    assert(symbol == '💩');
}

하지만 for … of 문은 문법차원의 요소이기 때문에 여기에 대해서는 폴리필이 없어요.

그 밖의 문제들

이 동작 방식은 사실 모든 문자열 메서드에 영향을 미칩니다. String.prototype.substring, String.prototype.slice 등 여기에 명시적으로 언급하지 않은 것들이 다 포함되죠. 그러니까 주의해서 사용하시길 바랍니다.

정규식에서의 문제들

코드포인트와 유니코드 스칼라 값에 매치시키기

정규식의 . 연산자는 '글자' 한 개에만 매칭됩니다. 하지만 자바스크립트는 대체쌍 반쪽을 각각 하나의 '글자'로 계산하기 때문에, 이 연산자는 아스트랄 기호에는 절대 매치되지 않아요.

>> /foo.bar/.test('foo💩bar')
false

잠시 생각을 해보죠... 모든 유니코드 글자에 매치시키기 위해 사용할 수 있는 정규식이 뭘까요? 떠오르세요? 보셨다시피 . 연산자로는 부족합니다. 여기에는 개행문자나 아스트랄 기호 전체가 매치되지 않으니까요.

>> /^.$/.test('💩')
false

개행문자에도 매치시키기 위해 [\s\S]를 쓸 순 있죠. 하지만 여전히 아스트랄 기호 전체는 매치되지 않습니다.

>> /^[\s\S]$/.test('💩')
false

보면 아시겠지만 모든 유니코드 코드포인트에 매치되는 정규표현식은 전혀 간단하지가 않아요.

>> /^[\0-\uD7FF\uDC00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF]$/.test('💩') // 이게 뭡니까 세상에
true

당연히 이런 정규식을 직접 쓰고 싶진 않으시겠죠. 디버깅이라면 더 말할 것도 없고요. 위의 정규식을 만들어내기 위해 저는 Regenerate을 사용했습니다. 코드포인트나 글자 목록을 가지고 정규식을 쉽게 생성해주는 라이브러리에요.

>> regenerate.fromCodePointRange(0x0, 0x10FFFF)
'[\0-\uD7FF\uDC00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF]'

왼쪽에서 오른쪽 순으로, 이 정규식은 BMP 기호, 아스트랄 기호의 대체쌍들, 그리고 대체쌍 반쪽짜리들에 매치됩니다.

대체쌍 반쪽짜리는 자바스크립트 문자열에서 기술적으로는 가능하지만 그 자체로는 어떠한 글자에도 매핑되지 않기 때문에 쓰면 안됩니다. 유니코드 스칼라 값이라는 용어는 대체 코드포인트를 제외한 나머지 모든 코드포인트를 가리킵니다. 유니코드 스칼라 값에 매치되는 정규식은 다음과 같이 생성할 수 있습니다.

>> regenerate()
     .addRange(0x0, 0x10FFFF)     // 모든 유니코드 코드포인트
     .removeRange(0xD800, 0xDBFF) // 첫번째 대체쌍(high surrogate) 제외
     .removeRange(0xDC00, 0xDFFF) // 두번째 대체쌍(low surrogate) 제외
     .toRegExp()
/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/

Regenerate은 빌드 스크립트 안에서 사용하도록 개발되었습니다. 복잡한 정규식을 생성해내면서도 해당 스크립트 자체는 높은 가독성을 유지하고 유지보수하기도 쉽도록 말이죠.

ES6에서는 희망적이게도 u 플래그가 도입됩니다. 이 플래그를 사용하면 . 연산자에 대체쌍 반쪽짜리를 제외한 나머지 모든 코드포인트가 매치됩니다.

>> /foo.bar/.test('foo💩bar')
false

>> /foo.bar/u.test('foo💩bar')
true

u 플래그를 설정하면 . 연산자는 다음의 하위호환 정규식 패턴과 동일하게 동작하게 됩니다.

>> regenerate()
     .addRange(0x0, 0x10FFFF) // 모든 유니코드 코드포인트
     .remove(  // 개행문자 제외
       0x000A, // 라인 피드 <LF>
       0x000D, // 캐리지 리턴 <CR>
       0x2028, // 행 구분자 <LS>
       0x2029  // 단락 구분자 <PS>
     )
     .toString();
'[\0-\x09\x0B\x0C\x0E-\u2027\u202A-\uD7FF\uDC00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF]'

>> /foo(?:[\0-\x09\x0B\x0C\x0E-\u2027\u202A-\uD7FF\uDC00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF])bar/u.test('foo💩bar')
true

문자 클래스의 아스트랄 범위

/[a-c]/라는 정규식은 U+0061 라틴 소문자 A부터 U+0063 라틴 소문자 C까지의 글자에 매치됩니다. 그렇다면 /[💩-💫]/U+1F4A9 개똥부터 U+1F4AB 현기증 기호까지 모든 기호에 매치되어야 하는데, 실제로는 그렇지가 않아요.

>> /[💩-💫]/
SyntaxError: Invalid regular expression: Range out of order in character class

이런 결과가 나오는 이유는 이 정규식이 다음 정규식과 동일하기 때문입니다.

>> /[\uD83D\uDCA9-\uD83D\uDCAB]/
SyntaxError: Invalid regular expression: Range out of order in character class

우리가 기대한 것은 U+1F4A9, U+1F4AA, U+1F4AB에 매치되는 건데 이 정규식은 다음과 같이 동작합니다.

  • U+D83D(첫 글자의 high surrogate)를 찾는다. 없으면...
  • U+DCA9(첫 글자의 low surrogate)에서 U+D83D(두번째 글자의 high surrogate)를 찾는다. (범위의 시작 코드포인트 값이 종료 코드포인트 값보다 크기 때문에 유효하지 않음) 없으면...
  • U+DCAB(두번째 글자의 low surrogate)를 찾는다.

ES6에서는 다시 한번 마법의 /u로 좀더 이치에 맞는 동작을 선택할 수 있습니다.

>> /[💩-💫]/u.test('💩') // U+1F4A9에 매치됨
true
>> /[💩-💫]/u.test('💪') // U+1F4AA에 매치됨
true
>> /[💩-💫]/u.test('💫') // U+1F4AB에 매치됨
true

하지만 이 방법은 ES5와 구 환경에 하위호환은 되지 않습니다. 하위호환을 위해서는 Regenerate을 사용해 ES5-호환 정규식을 생성하여 아스트랄 범위 또는 아스트랄 글자를 처리해야 합니다.

>> regenerate.fromSymbolRange('💩', '💫')
'\uD83D[\uDCA9-\uDCAB]'
>> /^\uD83D[\uDCA9-\uDCAB]$/.test('💩') // U+1F4A9에 매치됨
true
>> /^\uD83D[\uDCA9-\uDCAB]$/.test('💪') // U+1F4AA에 매치됨
true
>> /^\uD83D[\uDCA9-\uDCAB]$/.test('💫') // U+1F4AB에 매치됨
true

실무에서 버그 회피

이 동작방식 때문에 문제될 수 있는 것들이 많은데요. 예를 들면 트위터에서는 하나의 트윗에 140자까지 허용되고 백엔드는 아스트랄 기호를 포함한 모든 글자를 받아들입니다. 하지만 트위터 사이트의 자바스크립트 카운터는 대체쌍을 고려하지 않고 문자열의 length 프로퍼티만 읽었기 때문에 아스트랄 기호를 70자 이상 입력할 수가 없었어요. (지금은 이 문제가 수정되었습니다.)

문자열을 처리하는 수많은 자바스크립트 라이브러리들이 아스트랄 기호를 제대로 계산하지 못하고 있습니다.

Countable.js가 출시되었을 때도 아스트랄 기호를 제대로 세지 못했고요.

Underscore.string 역시 reverse 구현에서 유니코드 결합자나 아스트랄 기호를 제대로 처리하지 못하고 있습니다. (그러니 미시 엘리엇의 알고리즘을 사용하세요.)

&#x1F4A9;와 같이 아스트랄 기호를 표기하는 HTML 숫자 엔티티도 적절하게 디코드하지 못하고요. 다른 HTML 엔티티 변환 라이브러리들도 비슷한 문제를 가지고 있습니다.

(이 문제들이 수정되기 전까지는 HTML 인코딩/디코딩이 필요할 때 he를 사용해보세요.)

이런 실수들은 모두 쉽게 발생할 수 있습니다. 말하자면 결국 자바스크립트가 유니코드를 다루는 방식 자체가 밉상맞아요. 이 글에서는 문제가 생겼을 때 해결책들을 쭉 설명했는데요, 그럼 문제를 예방하려면 어떻게 해야 할까요?

개똥 테스트™를 소개합니다

자바스크립트 코드로 문자열이나 정규식을 다룰 일이 있다면 개똥 글자(💩)가 포함된 문자열을 처리하는 유닛테스트를 추가하세요. 그리고 뭔가 잘못되는 게 없나 확인해보는 거죠. 아스트랄 기호가 지원되고 있는지 확인하는 빠르고 재미있고 쉬운 방법입니다. 그리고 코드에서 유니코드 관련된 버그가 발견되면, 이 글에서 설명한 테크닉을 적용해 수정하시면 됩니다.

일반적인 유니코드 지원을 확인하는 데 좋은 테스트 문자열은 다음과 같습니다.

Iñtërnâtiônàlizætiøn☃💩

첫 20자는 U+0000 ~ U+00FF 범위 내의 글자들이고, 그 다음에는 U+0100 ~ U+FFFF의 글자, 그리고 마지막으로 U+010000 ~ U+10FFFF 범위 내의 아스트랄 기호가 들어있어요.

한 줄 요약 : 나가서 개똥글자가 들어있는 pull request를 올리세요. 유니코드를 통해 웹을 전진®시키는 유일한 방법입니다.

일러두기 : 이 글은 ES6의 최신버전 초안 및 자바스크립트의 유니코드 지원을 개선하고자 하는 다양한 앞잡이들과 제안서에 기반해 쓰여졌습니다. 여기 언급된 새로운 기능들 중 일부는 실제로 ES6 최종 명세에 포함되지 않을 수 있습니다.



window.open 설명 : https://www.w3schools.com/jsref/met_win_open.asp

window 객체 설명 (코딩의 시작) : http://www.tcpschool.com/javascript/js_bom_window


* Center로 띄우기

var popWidth = 495;
        var popHeight = 372;
        var popLeft = (screen.width / 2) - (popWidth / 2);
        var popTop = (screen.height / 2) - (popHeight / 2);

/*
window.screenLeft / window.screenTop
window.screenX / window.scrrenY

var scnLeft = window.screenLeft ? window.screenLeft : window.screenX;
var scnTop = window.screenTop ? window.screenTop : window.screenY;

screenLeft / screenTop 은 브라우저 Left / Top이 왼쪽으로부터 얼마나 떨어져 있는지 값이다.
듀얼 스크린인 경우 첫번째 스크린은 왼쪽부터 떨어져 있는 px이고 두번째 스크린은 첫번째 스크린으로부터 떨여져 있는 px 값이다.
center로 맞출 때는 해당 값을 사용하면 현재 브라우저의 가운데로 위치하게 된다.
*/

        var options = 'width=' + popWidth + ',height=' + popHeight + ',left=' + popLeft + ',top=' + popTop + ',location=no,menubar=no,resizable=no,status=no,toolbar=no';
        window.open(mobileUrl, 'mobileAppDownload', options);






===================================================================================================================================



팝업 (자식창) 에서 호출한 페이지 (부모창) 닫기



출처 : http://blog.naver.com/PostView.nhn?blogId=innoc99&logNo=140055334056


우선 JavaScript의 Window객체에는 Open()과 Close()라는 함수가 있다..

각각의 함수는 (다들 아시겠지만) 이름대로 브라우저 창을 열고 닫아 주는 역할을 한다.

Open()함수는 여기서 주가 아니므로 간단히 예만 보고 넘어가도록 하자..

 

  window.open("b.html","_blank","direction=yes, location=yes, menubar=yes, scrollbars=yes, status=yes, 

                         toolbar=no, resizeble=no, width=300, height=300");

 

이런 형태로 사용한다. 각각의 파라메터는 생략가능하다.

 

문제는 close()함수인데.. 사실 클로즈 함수 자체는 어려울게 없다. 대상윈도우에다가 사용하면 된다..예를 들어..

 

 자기 자신을 닫을 때는

 window.close();

 self.close();

 close();

 중에 하나..

 

 팝업이나 Open된 창에서 부모창을 닫을 때는

 window.opener.close();

 opener.close();

 등등을 사용하면 된다.

 

여기서 발생하는 문제가 Close()함수를 호출하면 창을 닫을 것인지 다이얼로그창으로 물어본다는것이다.

보통 Close()를 사용할때 물어보길 원할때도 있지만 대부분은, 조용히 닫혀지기를 원한다.  이때 사용가능한 편법이 있다..

닫혀지는곳의 opener를 닫혀지는곳 자신이나, null, ""등의 값으로 설정하면된다.. 말이 좀 어렵다.. 직접 보도록하자..

 

 자기자신을 닫을 때는

self.opener = self; 혹은 self.opener = null; 한 후

self.close()를 하면 된다..

 

반대로 팝업이나 Open된 창에서 부모창을 닫을 때는 부모창을 a.html 자식창을 b.html이라고 할때

a.html에서 self.opener = self; 혹은 self.opener = null; 을 설정한 후

오픈된 b.html에서 opener.close()를 호출하면 된다.

 

사실 좀 복잡할지 모르지만 한두번 해보면 금방익숙해 진다.

그런데.. 문제가 하나있다.. MS가 바보가 아니라.. 이편법을 IE7에서는 막아버렸다. 자식이 부모를 맘대로 죽이는(?) 하극상을 못참았나보다.

이방법을 사용해도 메시지 박스가 뜬다.. 하지만, 막는자가 있으면 뚫는자도 있는법.. 매우 참신한 방법으로 이 방법을 해결할 수있다..

 

 자기자신을 닫을 때는

 window.open("about:blank","_self").close();

 자기 자신에 윈도우를 하나열어서 자기 자식윈도우로 만든다음에 바로 닫아버린다.

 

 그렇다면 반대로 팝업이나 Open된 창에서 부모창을 닫을 때는 어떻게 할까..

  간단하다..

  opener.open('about:blank','_self').close();

  부모창에다 열고 닫아버리면 된다...






1. Fyneworks Multiple File Upload
URL : https://www.fyneworks.com/jquery/multiple-file-upload/


2. Blueimp jQuery File Upload
URL : http://blueimp.github.io/jQuery-File-Upload/


3. Uploadify
URL : http://www.uploadify.com/


4. Plupload
URL : http://www.plupload.com/

출처 : http://blog.naver.com/PostView.nhn?blogId=romeoyo&logNo=120188644234

Jquery UI 를 이용해 서비스 개발 작업을 하고 있는데. 어처구니없는 난관에 봉착했다.

무려 $99를 지불하고 구입한 RedActor html 에디터를 Jquery UI의 다이얼로그에 심었더니 이미지 URL삽입 같은 자체 UI의 입력란에 포커스가 가지 않는것이다.

무슨 유료 라이브러리가 이래 허접할까 툴툴대고 있었는데 범인은 Jquery UI 로 판명났다.

 

아래가 그 증상이다.

당신이 클릭의 신이어도 절대로 "이미지링크" 입력란에 포커스가 가지 않는다. (IE에서는 클릭하면 가긴 간다)

 

이틀정도 삽질하다가 구글신이 답을 주셨다.

아래가 정상적으로 포커스가 간 것이다.

 

Jquery, Jquery UI 라이브러리를 로딩하고 아래의 한줄을 추가해 주면 된다.


$.ui.dialog.prototype._focusTabbable = function(){};

 

잉글리쉬에 능숙하다면 아래 자료를 봐도 된다. Fix되었고, 관련 옵션이 추가되었다고 하는데. 내가 쓰는 Jquery UI 1.10.2 는 문제가 있으며, 공식 API문서에도 관련 옵션을 못찾겠다 꾀꼬리.

 

http://bugs.jqueryui.com/ticket/4731


$('form').on('submit', function() {

    $.mobile.activePage.find('.ui-btn-active').removeClass('ui-btn-active');

    // ajax - process

    return false;

});

1. 첫 페이지 호출 시 실행 순서

pagebeforechange

pagebeforechange

pagebeforecreate

pagecreate

pagebeforeshow

pageshow

pagechange 



2. 다른 페이지 Link 시 다음 페이지 실행 순서

pagebeforechange

pagecontainerbeforeload

pagebeforecreate

pagecreate

pagebeforechange

pagebeforehide

pagebeforeshow

pagechange

pageshow  


3. 다른 페이지 Link 시 처음 페이지 실행 순서

pagebeforechange

pagebeforechange

pagebeforehide

pagebeforeshow

pagechange

pageremove

pageshow 



4. 같은 페이지 Link 시 같은 페이지 실행 순서

pagebeforechange

pagebeforecreate

pagecreate

pagebeforechange

pagebeforehide

pagebeforeshow

pageshow

pagechange  


5. 같은 페이지 Link 시 두 번 이상 실행 시 실행 순서

pagebeforechange

pagebeforechange

pagebeforehide

pagebeforeshow

pageshow

pagechange


출처 : http://ygang.tistory.com/109

기본적으로 JavaScript 코드는 자신이 로드된 도메인만 통신이 가능하고, 크로스 도메인(다른 도메인)과는 통신할 수 없다. 이것은 동일 출처 정책이라고 한다. 이것은 AJAX 통신에도 그대로 적용된다. 보안 관점에서 동일 출처 정책은 악의적인 JavaScript 코드가 다른 도메인의 데이터에 대한 무단 액세스를 할 수 없도록 한다는 점에서는 매우 효과적이다.

이 정책은 두 가지 형태의 웹앱에서는 장애물이 된다.

첫번째는 여러가지 Open API에 접근해서 클라이언트 매시업 기능을 필요로 하는 웹액에서는 장애물이 된다.

두번째는 웹앱을 네이트브앱으로 만들 경우 기기의 로컬에서 로딩된 자바스크립트는 외부 서버에서 엑세스 할 수 없다. 

 

  <script type="text/javascript">
    $(document).bind("mobileninit",function(){
      $.support.cors =true;
      $.mobile.allowCrossDomainPages = true;
    });
 </script>

 

$.support.cors-true 는 jQuery의 $.ajax() 메소드가 크로스 도메인 페이지를 로드할 수 있도록 해준다. 그리고 $.mobile.allowCrossDomainPage=true 는 jQuery Mobile API가 크로스 도메인 페이지를 로드할 수 있도록 해 준다.

jQuery Mobile 웹액을 네이티브앱으로 변활할 경우 주의할 점은 시작 HTML(홈페이지)에 기술된 다른 페이지로의 링크 경로를 크로스 도메인 절대 경로로 변경해야 한다.


 

nstore_app_slide.zip


참고 : http://nstore.naver.com


 

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Insert title here</title>

<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css" />
<style>
body, input, textarea, select, button, table {
    font-family: '돋움',Dotum,Helvetica,sans-serif;
    font-size: 12px;
}
ul {list-style:none;}
a {text-decoration:none;}

.lst_thum_wrap {width:670px; float:left; margin:0 30px;}
.lst_thum_wrap .lst_thum {width:100%; margin:0; padding:16px 0 0 16px; position:relative;}
.lst_thum_wrap .lst_thum:after {display:block; clear:both; content:'';}
.lst_thum_wrap .lst_thum li {width:108px; margin:0; padding:0 26px 10px 0; float:left; position:relative; vertical-align:top;}
.lst_thum_wrap .lst_thum li .mask {display:block; overflow:hidden; position:absolute; width:102px; height:102px; background:url('images/sp_mask.png') no-repeat -325px 0;}
.lst_thum_wrap .lst_thum li .ico {top:-10px; left:-12px; position:absolute; z-index:20; width:41px; height:41px; background:url('images/sp_sub_common.png') no-repeat; font-style:normal;}
.lst_thum_wrap .lst_thum li .ico_new {background-position: -184px -299px;}
.lst_thum_wrap .lst_thum li .ico_free {background-position: -92px -299px;}
.lst_thum_wrap .lst_thum li .ico_gift {background:url('images/sp_ico.png') no-repeat -120px 0;}
.lst_thum_wrap .lst_thum li .ico_vari {line-height:22px !important; font-family:Tahoma !important; font-size:10px; font-weight:bold; padding-top:8px; color:#fff; height:41px; background-position:-46px -299px; text-align:center;}
.lst_thum_wrap .lst_thum li a img {width:101px; height:101px; margin:1px 0 5px 1px;}
.lst_thum_wrap .lst_thum li a img + strong {padding:5px 0 0; line-height:18px; word-break:break-all; display:block; color:#000; white-space:normal;}
.lst_thum_wrap .lst_thum li .writer {display:block; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;}
.lst_thum_wrap .lst_thum li .score {margin:0 0 0 -1px; display:inline-block; position:relative; width:61px; height:12px; background:url('images/sp_sub_common.png') no-repeat -732px -249px; vertical-align:middle; font-size:11px; font-weight:bold; color:#333;}
.lst_thum_wrap .lst_thum li .score span {display:block; overflow:hidden; position:relative; height:12px; background:url('images/sp_sub_common.png') no-repeat -732px -232px; line-height:999px;}
.lst_thum_wrap .lst_thum li .score_num {display:inline-block; position:relative; top:1px; font-weight:bold; font-size:11px; color:#333; vertical-align:middle; font-style:normal; left:-2px;}
.lst_thum_wrap .lst_thum li .price {display:block; margin: 4px 0 0 -2px; line-height:16px; text-align:left; white-space: normal;}
.lst_thum_wrap .lst_thum li .price strong {word-break:break-all; color:#1b8ffc;}


.flick-carousel {width:730px; position:relative; height:244px;}
.flick-carousel .flick-carousel-outer-container {overflow:hidden; position:absolute; width:730px; height:230px;}
.flick-carousel .flick-carousel-outer-container .flick-carousel-content-container {height:230px; width:10000px; white-space:nowrap;}
.flick-carousel a.flick-button-previous,
.flick-carousel a.flick-button-next {position:absolute; top:65px; width:27px; height:43px; background:url('images/sp_alsobought_bs_v2.png') no-repeat; line-height:999px; overflow:hidden; z-index:30; display:none;}
.flick-carousel a.flick-button-previous {background-position:5px -108px;}
.flick-carousel a.flick-button-previous:hover {background-position:-22px -108px;}
.flick-carousel a.flick-button-next {background-position:-49px -108px; right:0;}
.flick-carousel a.flick-button-next:hover {background-position:-76px -108px;}
.flick-carousel .flick-nav {position:absolute; bottom:6px; left:0; width:100%; margin:0; text-align:center; display:none;}
.flick-carousel .flick-nav strong,
.flick-carousel .flick-nav a {display:inline-block; overflow:hidden; width:7px; height:7px; margin:0; line-height:999px; vertical-align:middle; background:url('images/sp_alsobought_as.png') no-repeat;}
.flick-carousel .flick-nav strong {background-position:-108px -46px;}
.flick-carousel .flick-nav a {background-position:-119px -46px;}
</style>

<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="http://code.jquery.com/ui/1.10.2/jquery-ui.js"></script>
<script>
jQuery(function($) {
 $.fn.extend({
  flick : function(opt) {
   var settings = {
    speed : 400
   }
   $.extend(true, settings, opt);
   
   var _this = $(this);
   var _outer = _this.find('.flick-carousel-outer-container');
   var _content = _outer.find('.flick-carousel-content-container');
   var _items = _content.find('.lst_thum_wrap');
   var itemsLen = _items.length;
   if(itemsLen < 2) {
    return false;
   }
   var itemsAvg = Math.floor(itemsLen/2);
   var _btnPrev = _this.find('.flick-button-previous');
   var _btnNext = _this.find('.flick-button-next');
   var _btnNav = _this.find('.flick-nav');
   var pos = 0;
   var idx = 0;
   var posIdx = 0;
   var moveItem = 0;
   var movePos = 0;
   var outerWidth = _outer.width();
   var itemWidth = _items.eq(0).outerWidth(true);
   var contentWidth = itemWidth * _items.length;
   var lastPos = contentWidth - outerWidth;
   _content.width(contentWidth);
   _btnPrev.add(_btnNext).add(_btnNav).show();
   
   _btnPrev.on('click', function(e) {
    e.preventDefault();
    
    var nIdx = idx-1;
    if(nIdx < 0) {
     nIdx = itemsLen - 1;
    }
    setNav(nIdx);
   });
   
   _btnNext.on('click', function(e) {
    e.preventDefault();
    
    var nIdx = idx+1;
    if(nIdx >= itemsLen) {
     nIdx = 0;
    }
    setNav(nIdx);
   });
   
   function prev() {
    if(moveItem > 0) {
     for(var i=0; i<moveItem; i++) {
      _content.find('.lst_thum_wrap').last().prependTo(_content);
     }
     _outer.scrollLeft(pos + itemWidth * movePos);
     //console.log('이전 이동 > moveItem : ' + moveItem + ', pos : ' + (pos + itemWidth * movePos) + ' > ' + pos);
    }
    
    _outer.stop().animate({
     scrollLeft : pos
    }, {
     duration:settings.speed,
     easing : 'easeOutExpo'
    });
   }
   
   function next() {
    if(moveItem > 0) {
     for(var i=0; i<moveItem; i++) {
      _content.find('.lst_thum_wrap').first().appendTo(_content);
     }
     _outer.scrollLeft(pos - itemWidth * movePos);
     //console.log('다음 이동 > moveItem : ' + moveItem + ', pos : ' + (pos - itemWidth * movePos) + ' > ' + pos);
    }
    
    _outer.stop().animate({
     scrollLeft : pos
    }, {
     duration:settings.speed,
     easing : 'easeOutExpo'
    });
   }
   
   function setNav(nIdx) {
    var _navBefore = _btnNav.children().eq(idx);
    _navBefore.replaceWith($('<a />', {href : '#'}).text(idx));
    var _navAfter = _btnNav.children().eq(nIdx);
    _navAfter.replaceWith($('<strong />').text(nIdx));
    
    movePos = nIdx - idx;
    if(movePos > 0) {
     if(movePos <= itemsAvg) {
      posIdx += movePos;
      if(posIdx >= itemsLen) {
       moveItem = posIdx - itemsLen + 1;
       posIdx = itemsLen - 1;
       pos = lastPos;
      } else {
       moveItem = 0;
       pos += itemWidth * movePos;
      }
      //console.log('다음(큰) > moveItem : ' + moveItem + ', posIdx : ' + posIdx);
      next();
     } else {
      var befPosIdx = posIdx;
      posIdx = posIdx + movePos - itemsLen;
      movePos = befPosIdx - posIdx;
      if(posIdx < 0) {
       moveItem = Math.abs(posIdx);
       posIdx = 0;
       pos = 0;
      } else {
       moveItem = 0;
       pos -= itemWidth * movePos;
      }
      //console.log('이전(큰) > moveItem : ' + moveItem + ', posIdx : ' + posIdx);
      prev();
     }
    } else {
     movePos = Math.abs(movePos);
     if(movePos > itemsAvg) {
      var befPosIdx = posIdx;
      posIdx = posIdx - movePos + itemsLen;
      movePos = posIdx - befPosIdx;
      if(posIdx >= itemsLen) {
       moveItem = posIdx - itemsLen + 1;
       posIdx = itemsLen - 1;
       pos = lastPos;
      } else {
       moveItem = 0;
       pos += itemWidth * movePos;
      }
      //console.log('다음(작은) > moveItem : ' + moveItem + ', posIdx : ' + posIdx);
      next();
     } else {
      posIdx -= movePos;
      if(posIdx < 0) {
       moveItem = Math.abs(posIdx);
       posIdx = 0;
       pos = 0;
      } else {
       moveItem = 0;
       pos -= itemWidth * movePos;
      }
      //console.log('이전(작은) > moveItem : ' + moveItem + ', posIdx : ' + posIdx);
      prev();
     }
    }
    idx = nIdx;
   }
   
   $(_btnNav).on('click', 'a', function(e) {
    e.preventDefault();
    var _this = $(this);
    setNav(parseInt(_this.text(), 10));
   });
   
  }
 });
 
 $('.flick-carousel').flick();
});
</script>

</head>
<body>

<div class="flick-carousel">
 <a href="#" class="flick-button-previous"></a>
 <div class="flick-carousel-outer-container">
  <div class="flick-carousel-content-container">
   <div class="lst_thum_wrap a0">
    <ul class="lst_thum">
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_new"></em>
       <img src="images/app1.png" />
       <strong class="bb">000</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app2.png" />
       <strong class="bb">[건비트2] 음악 게임의 신세계</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_gift"></em>
       <img src="images/app3.png" />
       <strong class="bb">이츄, 사랑의 홍차 연구소</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_free"></em>
       <img src="images/app4.png" />
       <strong class="bb">미투데이</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app5.png" />
       <strong class="bb">앱</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
    </ul>
   </div>
   <div class="lst_thum_wrap a1">
    <ul class="lst_thum">
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_new"></em>
       <img src="images/app6.png" />
       <strong class="bb">111</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app7.png" />
       <strong class="bb">네이버 블로그</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_gift"></em>
       <img src="images/app8.png" />
       <strong class="bb">이츄, 사랑의 홍차 연구소</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_free"></em>
       <img src="images/app9.png" />
       <strong class="bb">미투데이</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app10.png" />
       <strong class="bb">앱</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
    </ul>
   </div>
   <div class="lst_thum_wrap a2">
    <ul class="lst_thum">
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_new"></em>
       <img src="images/app11.png" />
       <strong class="bb">222</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app12.png" />
       <strong class="bb">네이버 블로그</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_gift"></em>
       <img src="images/app13.png" />
       <strong class="bb">이츄, 사랑의 홍차 연구소</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_free"></em>
       <img src="images/app14.png" />
       <strong class="bb">미투데이</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app15.png" />
       <strong class="bb">앱</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
    </ul>
   </div>
   <div class="lst_thum_wrap a3">
    <ul class="lst_thum">
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_new"></em>
       <img src="images/app11.png" />
       <strong class="bb">333</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app12.png" />
       <strong class="bb">네이버 블로그</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_gift"></em>
       <img src="images/app13.png" />
       <strong class="bb">이츄, 사랑의 홍차 연구소</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_free"></em>
       <img src="images/app14.png" />
       <strong class="bb">미투데이</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app15.png" />
       <strong class="bb">앱</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
    </ul>
   </div>
   <div class="lst_thum_wrap a4">
    <ul class="lst_thum">
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_new"></em>
       <img src="images/app11.png" />
       <strong class="bb">444</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app12.png" />
       <strong class="bb">네이버 블로그</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_gift"></em>
       <img src="images/app13.png" />
       <strong class="bb">이츄, 사랑의 홍차 연구소</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <em class="ico ico_free"></em>
       <img src="images/app14.png" />
       <strong class="bb">미투데이</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
     <li>
      <a href="#" title="앱이름">
       <span class="mask"></span>
       <img src="images/app15.png" />
       <strong class="bb">앱</strong>
      </a>
      <span class="writer">카테고리</span>
      <span class="score">
       <span style="width:84%;">평점</span>
      </span>
      <em class="score_num">4.2</em>
      <span class="price">
       <strong>무료</strong>
      </span>
     </li>
    </ul>
   </div>
  </div>
 </div>
 <a href="#" class="flick-button-next"></a>
 <span class="flick-nav">
  <strong>0</strong>
  <a href="#">1</a>
  <a href="#">2</a>
  <a href="#">3</a>
  <a href="#">4</a>
 </span>
</div>


</body>
</html> 

 

+ Recent posts