IT/언어

[JavaScript] JavaScript의 작성법

개발자 두더지 2020. 10. 6. 15:58
728x90

변수/리터럴


const를 먼저 사용한다.

 예전에는 변수 선언에 사용할 수 있는 것은 var뿐이었지만, 현재 먼저 사용해야할 것은 const이다. var을 전부 const로 바꿔 사용하며 덮어쓰기(변수의 변경)가 필요한 곳은 let을 사용한다.

const name = "@wozozo";

 또한 C++의 const를 아는 사람에게는 위화감이 있을지도 모르겠지만, const로 선언한 변수에 저장된 배열은 다시 값을 저장할 수는 없지만 배열에 요소를 추가하는 것은 가능하다. 오브젝트의 속성 변경도 가능하다. 그러므로 사용할 수 있는 곳이 상당히 넓다.

 그리고 var은 글로벌 스코프에 변수를 두는 경우에 여전히 사용할 수 있지만, 뒤에서 추가적으로 설명하겠지만 진정한 의미의 글로벌 스코프를 취급할 기회가 줄어들고 있으므로 기본적으로 "사용하지 않는 것"이 좋다.

변수의 스코프

 { , } 블록은 변수의 범위와 연결되는 개념이 아니었다.

 아래는 예전의 스타일의 코드이다.

for (var i = 0; i < 10; i++) {
    // do something
}
console.log(i); // -> 10

 아래는 현재 스타일의 코드로 let, const는 이 블록의 영향을 받는다.

for (let i = 0; i < 10; i++) {
    // do something
}
console.log(i); // ReferenceError: i is not defined

 if 또는 for과 같은 제약구문을 사용하지 않고 서서히 { , }의 블록을 써서 스코프를 제약하는 것이 가능하다. 스코프가 좁아지면 변수의 영향 범위가 작아지므로 코드를 이해하기 쉬워진다. 

 

문자열


문자열 ▶ 숫자의 변환

 이전에는 앞에 "0"을 넣으면 8진수로 다뤄져 의도하지 않았던 숫자가 되어버리곤 했기 때문에, radix를 반드시 붙여라는 말을 입이 닳도록 하였다. 특히 외부 입력을 다루는 경우에 주의할 필요가 있었다.

예전 코드는 아래와 같다.

// 8진수로 다뤄지는 것을 방지하기 위해 radix을 명시적으로 넣었다.
var age = parseInt(document.personForm.age, 10);

 명시적인 16진수 (0x이 앞에 붙인 것) 는 그대로 16진수가 되지만, ES5의 규정에 따라 0이 앞에 붙은 경우도 10진수로 될 수 있도록 되었다.  새로운 코드는 아래와 같다.

// 아무것도 하지 않고도 8진수로 다루어지지 않게 되었다.
const age = parseInt(document.personForm.age);

문자열의 결합

 기존에는 다른 언어로 말하자면 printf계와 같은 것이 없어 문자열을 "+"로 결합하거나 배열에 넣어 .join()으로 결합하거나 하였다. 그러한 예는 아래와 같다.

console.log("[Debug]:" + variable);

 현재에서는 문자열 템플릿 리터럴이라는 것이 있으므로 이것을 사용한다. prinf와 같이 숫자의 변환등의 포맷이 아닌 어디까지나 문자열 결합을 똑똑하게 하기 위한 것이다. 물론 숫자가 정해지지 않은 배열 등은 기존과 같이 .join()을 사용한다.

console.log(`[Debug]: ${variable}`);

 그러나 URL의 결합에 템플릿 리터럴을 사용해서는 안된다. URL을 작성할 때 아래와 같이 작성하면 얼마나 편리할지 생각해보아라.

const url = `/users/${id}`;

 하지만 방금 말했듯, 문자열의 중간에 체크하지 않고 결합하는것은 URL 인젝션의 온상이 된다. 서버측의 구현에 따라서는 (리다이렉터처로써 직접 이용하고 있는 경우) 커다란 문제로 연결될 가능성이 있다. 가장 간단한 방법으로는 변수를 전개하기 전에 encodeURIComponent를 통과하도록 하는 회피방법이 있다. 

 그럼에도 템플릿 리터럴을 사용하고 싶다면, backquote전에 함수 호출을 하는 기법의 tagged template literal이라는 것이 있다. 리터럴의 전에 키워드(함수명)을 두면 특별한 후처리가 가능하다. 따라서 여기서 인젝션을 예방하기 위한 체크나 회피 로직을 실행시킬 것이다.

 이러한 것을 실행하는 라이브러리를 찾지 못했기 때문에 엉성하지만 작성해본 코드는 다음과 같다. 1번째의 인수에는 ${}로 감싼 영역 이외의 문자열의 배열(과 같은 것)이 온다. 그 후에는 가변 인자로 전달된 인수를 취득할 수 있다.

const urljoin = (strings, ...values) => {
  const result = [];
  for (let i = 0; i < strings.length - 1; i++) {
    result.push(strings[i], encodeURIComponent(values[i]));
  }
  result.push(strings[strings.length - 1]);
  return result.join("");
}

 다음과 같이 사용한다.

const group_id = 10;
const member_id = 280;

const url = urljoin`/groups/${group_id}/members/${member_id}`;

 위와 같은 코드로 그렇지 좋지 않으므로 이 태그(함수)를 붙이지 않아도 정상적으로 통과해버려서 URLSearchParmams상당의 처리도 함께 해주는 거나, 템플릿 리터럴 이상의 기능을 넣어준다는 등 처리가 필요한 느낌이 든다.

 

오브젝트의 복제


REdux로 immutable.js를 사용하지않는 등의 상황에서는 오브젝트의 복사가 발생한다.

예전 스타일의 코드는 다음과 같다.

var destObj = {};
for (var key in srcObj) {
  if (srcObj.hasOwnProperty(key)) {
    destObj[key] = srcObj[key];
  }
}

요즘에는 Object.assgin()이라는 클래스 메소드를 사용한다.

const destObj = {};
Object.assign(destObj, srcObj);

ECMAScript2018에서는 오브젝트의 스프레드 연산자 서포트가 공식적으로 도입되게 되었다. 이 피리오드 (스프레드 연산자) 3개는 후에 살펴 볼 배열이나 인수의 부분에서도 나온다. 또한, 이 스레드 연산자는 마지막에 설명할 분할 대입의 '남은 요소'를 다를 때도 사용한다.

const destObj = {...srcObj};

 

클래스 선언


 예전에는 함수와 protoype이라는 속성을 이렇게 저렇게 다뤄서 표현하였다. 정확히는 클래스이지 않지만 코드의 유저 시점에서는 다른 언어의 클래스와 동등하므로 클래스로써 불렀다. 예전의 클래스의 표현은 다음과 같았다.

// 함수이지만 컨스터럭터
function SmallAnimal() {
    this.animalType = "ポメラニアン";
}

// 다음과 같이 계승
SmallAnimal.prototype = new Parent();

// 다음과 같이 메소드
SmallAnimal.prototype.say = function() {
    console.log(this.animalType + "이지만~");
};

var smallAnimal = new SmallAnimal();
smallAnimal.say();

 현재의 작성법은 다음과 같이 class를 사용한다. 

class SmallAnimal extends Parent {
    constructor() {
        this.animalType = "ポメラニアン";
    }

    say() {
        console.log(`${this.animalType}だけどMSの中に永らく居たBOM信者の全身の毛をむしりたい`);
    }
}

 

함수 선언


애로우 함수만을 사용한다.

 function 키워드는 버리자 ! function 키워드의 this의 취급은 트러블의 원인이 된다. 따라서 원래 존재하지 않았던 것처럼 묻어두자. 다음의 작성법은 예전에 사용하던 function 키워드를 사용한 것이다. 

function name(인수) {
    본체
}

 현재는 애로우 함수를 사용하여 작성한다 특히 무명함수와의 상성이 매우 높아졌다.

const name = (인수) => {
    본체
};

 상황에 따라서 괄호나 return 등이 생략되기도 한다.

즉시 실행 함수는 더 이상 사용하지 않는다.

 함수를 만들어 그 곳에 실행해 스코프 외에 변수 등이 보이지 않게 하는 것과 같은 테크닉이 예전에 사용되었다. 그리고 이러한 함수를 즉시 실행 함수라고 불렀다.  현재 시점에서는 WebPack나 Browserify나 Rollup나 Parcel등으로 파일을 결합하기도하고 예전과 같이 1개 파일에 라이브러리를 두고 그것을 <script>태그로 읽어들이는 것을 줄이고 있다고 생각된다. 

 그러므로 아래와 같은 작성법 자체도 줄어들고 있다. 

var lib = (function() {
  var libBody = {};

  var localVariable;

  libBody.method = function() {
      console.log(localVariable);
  }
  return libBody;
})();

 function(){}를 괄호로 묶어, 그 끝에 함수를 호출하기 위한 ()가 더욱이 붙어있는 느낌이다. 이것으로 export하고 싶은 특정의 함수만을 return으로 반환하여 공개하였다.

 현 시점의 ES6 스타일에서는 export { name1, name2, …, nameN };와 같은 작성법을 사용할 수 있다. Browserify/Node.js에서도 module.exports = { name1: name1, name2: name2... }이된다.

 

비동기 처리


JavaScript에서 중급이상이 되면 피할 수 없는 것이 비동기처림이다. 이전에는 콜백 지옥이라면 야유받는 느낌이었다. ( 또한, 에러 처리할 때는 괄호없는 if문을 하나의 행에 쓰는 스타일은 호불호가 갈리기 때문에 여기서는 설명하지 않는다.)

 콜백지옥의 예는 다음과 같다.

func1(引数, function(err, value) {
  if (err) return err;
  func2(引数, function(err, value) {
    if (err) return err;
    func3(引数, function(err, value) {
      if (err) return err;
      func4(引数, function(err, value) {
        if (err) return err;
        func5(引数, function(err, value) {
          // 最後に実行されるコードブロック
        });
      });
    });
  });
});

 그 이후에 비동기 처리를 하기에는 Promise를 사용하게 되었다. 이것으로 괄호가 얕아지게 되므로 쓰기 쉬워진다.

const getData = (url) => {
    fetch(url).then(resp => {
        return resp.json();
    }).then(json => {
        console.log(json);
    });
}; 

 단지, new Promise(...)에 관해서라면 Promise전에 new를 붙인 작성법도 최후에 콜백을 받는 함수 이외(setTiemout과 같은)를 다루는 경우를 제외하고 기본적으로 사용하는 것이 거의 없을 것이다. Promise는 async함수가 return도 함께 만든 것이다. 

 현시기에는 async/await 키워드를 에로우 함수의 전에 부여한다. function의 앞에도 붙일 수 있지만 function의 사용은 NG라는 것을 잊지말자.

 더욱이 발전한 현 시대의 작성법은 다음과 같다. async를 붙이면 그 함수가 Promise라는 클래스의 오브젝트를 리턴해주게 된다. Promise는 그 이름대로 '무거운 처리가 끝나면 나중에 부를게'라는 약속이다. await는 그 약속이 지켜질 것을 조용히 기다린다.

// 비동기 처리를 await에서 기다림
const fetchData = async (url) => {
    const resp = await fetch(url);
    const json = await resp.json();
    console.log(json);
};

 또한, 대부분의 await의 예에서는 반드시 리턴값을 변수에 넣거나 하지만 아래의 sleep과 같이 반드시 대입할 필요는 없다. 

const sleep = time => {
    return new Promise(resolve => {
        setTimeout(resolve, time);
    });
};

await sleep(100);

 Promise를 반환한 메소드에서는 Promise의 then()메소드를 부르는 것으로 약속을 지키는 것이 가능하지만, 가능한 await를 사용하여 then()사용도 줄이자.

 Promise.all()등의 곳에서 Promise를 사용하므로 그 이름을 금지하는 것은 아니다. 콜백 함수를 받을 수 있도록 작성된 함수를 사용할 경우는 Promise화한다. 위의 코드와 같이 new Promise를 사용하여 랩핑하는 것도 가능하지만 맨 마지막에 콜백, 콜백의 인수로Error를 사용하는 2010년대의 품행이 단정한 API라면 Promise화시켜주는 라이브러리가 있다. 

 Node.js 표준 라이브러리의 promisify를 사용하면 다음과 같이 작성할 수 있다.

const { promisify } = require("util"); 
const { readFile } = require("fs");
const readFileAsync = promisify(readFile);

const content = await readFileAsync("package.json", "utf8");

 Node.js이외에도 npm에 관련해서도 굉장히 많다. new Promise 자체도 결국은 콜백 스타일이되어버리므로 이러한 라이브러리를 사용하거나 랩핑한 함수를 라이브러리화하여 async/await를 콜백의 스타일이 코드 중에 섞이지 않도록 철저히 하자.

 

apply()


 예전에는 함수에 인수 세트를 배열로 꺼내고 싶을 때 apply()라는 메소드를 사용했었다.

function f(a, b, c) {
    console.log(a, b, c);
}

// a=1, b=2, c=3로써 실행된다.
f.apply(null, [1, 2, 3]);

 이 함수 속에 this를 최초의 인수로 지정하거나 하였지만 함수 선언을 모두 애로우 함수로 할 경우 이것은 과거의 이야기가 된다. 배열 공개의 문법의 스레드 연산자 ...를 사용하여 다음과 같이 동일한 동작을 할 수 있게 할 수 있다.

const f = (a, b, c) => {
    console.log(a, b, c);
};

f(...[1, 2, 3]);

 

디폴트 인수


 JavaScript는 같은 동적인 언어인 Python보다더 유연하게 선언된 인수를 붙이지 않고 호출하는 것이 가능하여, 예전에는 변수에 undefined라고 설정하거나 했었다. 이 때의 undefined 설정은 생략된 것으로 간주하여 값을 마음대로 설정하거나 했었다. 이와 관련된 코드는 다음과 같다.

// 디폴트 인수에 관한 예전 스타일의 코드

function f(a, b, c) {
    if (c === undefined) {
        c = "default value";
    }
}

 이러한 스타일의 코드의 경우 콜백 함수가 끝에 있고 도중의 값이 생략 가능하도록 할 때 귀찮아진다. JavaScript에는 마지막의 인수가 콜백한다는 것이 통일된 설계 방침으로 넓게 알려져 있기 때문에 (고대의 setTimeout와 같은 예외도 있지만), 아래와 같은 인수처리가 필요하거나 했다.

// 낡고 귀찮은 콜백 함수 다루기

function f(a, b, cb) {
    if (typeof b === "function") {
        cb = b;
        b = undefined;
    }
}

 어떤 인수가 생략가능한지 파악하고 생략했을 경우 인수를 대입하거나 하는 것이 귀찮고 같은 데이터 형의 인수가 있는 경우 판별이 잘 되지 않거나 하는 문제점등이 있다.

 따라서 현시대에는 다른 언어와 같이 함수 선언하는 곳에 작성하는 것이 가능해 복잡한 콜백을 직접 구현할 필요가 없어지게 되었다. 또한 콜백에 대한 것인데 이전에 설명한 것과 같이 Promise를 리턴하는  방법이 일반적이므로 '끝에는 함수이지만 도중에 생략'하는 경우가 없어졌다. 

// 새로운 디폴트 인수

const f = (name="小動物", favorite="ストロングゼロ") => {
  // ...
}

 배열이나 오브젝트는 분할 대입하는 기능이 증가하였지만, 이것을 함께 사용하면 오브젝트에 유연하게 파라미터를 받을 수 있고 디폴트 값도 설정되어있어 짐작하는 것도 가능하다. 

 또한 이전에는 선택적 인수는 opts이라는 이름의 오브젝트로 넘겨 주는 경우가 종종 있었다. 현 시점에서는 완전히 생략할 때도 디폴트 값을 설정하고 부분적으로 설정 가능한 것처럼 보이는 인수도 다음과 같이 작성할 수 있다. 오브젝트가 아닌 배열로도 할 수 있다.

// 분할 대입을 사용해 배열이나 오브젝트를 변수에 전개 혹은 디폴트 값으로 설정
const f = ({name="小動物", drink="ストロングゼロ"}={}) => {
  // ...
}

 

 

this를 조작하는 코드를 작성하지 않는다.


 이전에는 prototype을 조작하지 않도록 상속함수를 만들거나 하는 등의 해서는 안 될 프로그래밍을 하는 경우가 있었다. 또한 동일한 함수를 만들어 유용하고 있다는 점 등으로 this를 의식한 코딩을 자주 했었다. 예부터 this를 실행시에 대체함으로써 유연성을 취득해 온 ES3~5까지 해온 것이다. 예부터 이러한 this의 차이를 알고 아룰 수 있는 것이 JavaScript의 상급자의 첫 걸음일 정도였다.

// 예전 스타일의 코드

// apply()의 첫 번째 인수로 this를 외부에서 지정하여 실행
func.apply(newThis, [1, 2, 3]);

// call()의 첫 번째 인수로 this를 외부로부터 지정하여 실행
func.call(newThis, 1, 2, 3);

// bind()로 this를 고정
func.bind(newThis);

// 오브젝트에 속해 있는 함수 오브젝트는 오브젝트가 this에 대입되어 실행된다. 
var obj = {
    method: function() {
    }
};

// 아무것도 포함되어 있지 않으면 global 이름 공간 (브라우저라면 window와 같은)을 표시.
// 글로벌 이름 공간에 변수 추가("오염"이라고도 함)되어 버리게 된다.
function global() {
    this.bucho = "show";
}

 특히 제일 마지막 부분이 귀찮은 것으로 이벤트 핸들러를 오브젝트 내부에 정의했을 때에 그 오브젝트를 참고하는 방법이 없어지기 때문에 다음과 같은 코드가 작성되었다. 

// 이후에 필요없어지는 관용구

var self=this;

 jQuery에도 이와 비슷한 흔적이 있다. jQuery의 this는 선택된 현재 노드를 표시한다.

$('.death-march').each(function () {
  $(this).text("@moriyoshi参上");
});

 물론 사용하고 있는 프로엠워크가 특정의 방식을 기대하고 있다면 this를 사용하지 않는 것은 후에 문제를 유발할 수 있다. 

 아무튼 이러한 this의 조작은 현재는 불필요한 것이 되었다. 화살표 연산자를 사용하면 오브젝트의 안이라도 항상 인스턴스를 표시하게 된다. 따라서 var self = this는 필요없다.

 또한 오브젝트에 메소드를 추가하는 것에는 다음의 구문을 사용할 수 있다. function 키워드를 사용하여 작성하면 실제 큰 차이가 없지만 반드시 쓰지 않아도 되기 때문에 편하다. 클래스도 오브젝트도 항상 this가 인스턴스가 되면 이 this는 어디서 온 것일까와 같은 고민을 할 필요가 없어진다.

// ES6의 오브젝트의 메소드는 생략 기법이 있다.

const obj = {
    method() {
        console.log(this);
    }
}

 

배열과 사전형


 ES6에서는 단순 배열 이외에도 Map/Set등이 증가하였다. 이것들은 자식 데이터를 플랫 형식으로 많이 넣을 수 있는 데이터 구성으로 루프를 이용해 하나씩 데이터를 취득하는 것이 가능하므로 iterable이라고도 부른다. 그러므로 배열 고유의 조작이 아닌 iterable 공유의 조작으로 하는 것이 2018년도의 ES의 작성법이 된다.

루프는 for .... of 를 사용한다.

예전에는 for 구문을 다음과 같이 작성했다.

// 예전 스타일의 루프문(1)

var iterable = [10, 20, 30];

for (var i = 0; i < iterable.length; i++) {
  var value = iterable[i];
  console.log(value);
}

 다음의 코드는 비교적 새로우나 이것보다 더욱 새로운 코드 작성법이 있다. 일단 현재의 iterable (Array, Set, Map)한 것을 사용할 수 있다. 단지 퍼포먼스상 느리거나 한다.

// 예전 스타일의 루프(2)

var iterable = [10, 20, 30];

iterable.forEach(value => {
  console.log(value);
});

 이터럴 프로토콜이라는 언어 조합이 이러한 것이다. 아래는 새로운 스타일의 코드이다.

const iterable = [10, 20, 30];

for (const value of iterable) {
  console.log(value);
}

 이러한 것은 함수 호출을 수반하지 않는 얕은 코드이므로 async/awit도 함께 사용할 수 있다. 배열의 요소를 인수로 하여 하나씩 async하고 싶은 경우에 말이다.

// 새로운 코드 스타일과 async

const iterable = [10, 20, 30];

for (let value of iterable) {
  await doSomething(value);
}

사전, 배쉬 용도로 오브젝트가 아닌 Map을 사용한다.

예전의 코드는 오브젝트를 다른 언어의 사전이나 배쉬와 같이 작성하였다.

// 예전의 코드

var map = {
  "五反田": "約束の地",
  "戸越銀座": "TGSGNZ"
};

for (var key in map) {
    if (map.hasOwnProperty(key)) {
        console.log(key + " : " + map[key]);
    }
}

현재는 Map를 사용한다.

// 현재의 코드

const map = new Map([
  ["五反田", "約束の地"],
  ["戸越銀座", "TGSGNZ"]
]);

for (const [key, value] of map) {
    console.log(`${key} : ${value}`);
}

 이전과 같이 key만으로 루프를 돌리고 싶은 경우는 for (const key of map.keys()), value만으로 루프하고 싶은 경우는 for (const value of map.values())를 사용할 수 있다.

 keys()메소드, values()메소드도 배열의 실체를 만들고 있는 것이 아니라 반복자라는 작은 오브젝트만을 리턴하기 때문에 요소 수가 많아져도 가볍게 동작할 수 있다. 

분할 대입 (Destructuring Assignment)

 오브젝트나 배열의 안에 전개하는 방법으로써 예전에 하나씩 변수에 대입하거나 오브젝트 그대로 다루는 방법을 사용하였다. 

// 하나씩 꺼내거나 꺼내지 않고 오브젝트 그대로 사용하는 기존의 방법

var thinking = {
    name: "小動物",
    mind: "Python3と寝たい",
    reason: "`raise e from cause` べんりですよ"
};
console.log(thinking.name + "だけど" + thinking.reason + " " + thinking.mind + "の理由の一つです");

var name = thinking.name;
var mind = thinking.mind;
var reason = thinking.reason;
console.log(name + "だけど" + reason + " " + mind + "理由の一つです");

 분할 대입을 사용하면 오브젝트나 배열을 한 번에 복수의 변수에 전개할 수 있다. 오브젝트의 경우는 변수명과 키 명으로 꺼낼 수있다. 대응하는 요소가 존재하지 않을 경우에의 디폴트 값도 자유롭게 설정할 수 있다.

// 분할 대입하고 디폴트 값도 함께 설정할 수 있다.

const thinking = {
    name: "小動物",
    mind: "Python3と寝たい",
    reason: "`raise e from cause` べんりですよ"
};

const {name="約束の地の住人", mind, reason} = thinking;
console.log(`${name}だけど${reason} ${mind}理由の一つです`);

 작성법은 함수의 인수의 디폴트 값 설정과 동일하다.

 import 문이나 require문의 경우 한 번 호출시에 하나의 값 밖에 리턴되지 않는다. 복수의 값을 얻고 싶을 때는 1개로 묶은 오브젝트를 받은 후에 속성 접근하거나 몇 번이고 호출하거나 하였다.

// 분할 대입을 사용하지 않은 예전 작성법

var path = require("path");
var readFileSync = require("fs").readFileSync;
var writeFileSync = require("fs").writeFileSync;

 이렇게 작성하면 중간에 변수를 생략하여 원하는 것만을 얻을 수 있게 된다. 하지만 async/await는 하나 밖에 값을 리턴하지 않기 때문에, 아래와 같이 작성하면 복수의 값을 가볍게 리턴할 수 있게 된다.

// 분할 대입을 사용하여 한번에 원하는 것만을 취득하는 작성법

const { join } = require("path");
import { readFileSync, writeFileSync } from "fs";

 분할 대입의 좌변에 스프레드 연산자를 두는 거승로 '남은 요소'를 다룰 수 있다. 오브젝트의 스프레드 연산자는 ECMAScript 2018에 공식 사양으로 포함되어 있다. 

 예전에는 배열의 slice를 사용하는 방법이었다. 오브젝트의 경우는 간단한 방법이 없어서 오브젝트를 복사한 후 불필요한 요소를 삭제하거나 키를 이용해 루프를 돌려 필요한 요소만을 새로운 객체에 할당하는 조작을 하였다.

// 예전의 배열관련된 방법
var rest = array.slice(2);

 스프레드 연산을 사용하면 보다 쉽게 기재할 수있다.

// 배열
const [ a, b, ...rest ] = array;
// 오브젝트
const { a, b, ...rest } = obj;

참고자료

qiita.com/shibukawa/items/19ab5c381bbb2e09d0d9

 
728x90