[javascript] 스터디 1주차 - 자바스크립트 코딩의 기술 1-3장 요약

8 minute read

학원 수료를 마치고 신입 개발자로서 프런트엔드 업무를 맡게되었다. 역시나 걸리는 점은… 자바스크립트가 약하다는 생각이다. 그래서 비슷한 고통을 겪는 학원 동기들과 스터디를 하게 됐다.

스터디에 사용할 책은 자바스크립트 코딩의 기술(조 모건 저).

총 10장 구성이며, 1주차인 이번주는 3장까지 요약정리한다.

1. 변수 할당으로 의도를 표현하라.

과거에는 var을 통해서만 변수를 할당해였지만, ES6+ 부터는 constlet가 추가되었다. var은 어휘적 유효범위(함수 유효범위)를 따르기 때문에 의도치 않은 결과를 얻을 수 있지만, constlet블록 유효범위를 따르기 때문에 프로그래머가 예상한 결과를 얻을 수 있다.

1.1. 블록 유효범위로 선언하자.

유효범위 충돌 - 예제코드

다음은 배열의 각 요소에 함수를 연결하는 예시코드이다.

function addClick(items){
    for(var i = 0; i < items.length; i++){
        items[i].onClick = function(){
            return i;
        };
    }
    return items;
}
const example = [{}, {}];
const clickSet = addClick(example);
clickSet[0].onClick(); // 2
clickSet[1].onClick(); // 2

인덱스에 해당하는 return값을 출력 시키고자 하는 의도로 짜여졌지만 onClick()함수는 2만 출력한다. 이는 for문에 iterator로 사용하는 변수가 var로 선언되었기 떄문이다.

function addClick(items){
    for(let i = 0; i < items.length; i++){
        items[i].onClick = function(){
            return i;
        };
    }
    return items;
}
const example = [{}, {}];
const clickSet = addClick(example);
clickSet[0].onClick(); // 0
clickSet[0].onClick(); // 1

위 처럼 let으로 바꾸어주었더니 의도한 대로 출력되는 것을 확인할 수 있다.

1.2. 템플릿 리터럴을 사용하자.

전통적으로 변수를 포함한 문자열을 리턴할 때 +연산자를 사용하여 연결시켜주었는데, 템플릿 리터럴을 사용하면 편리하게 연결해서 사용할 수 있다.

function generateLink(image, width){
    const widthInt = parseInt(width, 10);
    return 'http://' + getProvider() + '/' + image + '?width=' + widthInt;
}

위 코드는 URL을 생성하는 함수인데, 간단한 URL임에도 불구하고 지저분하다. 이 때 백틱 (`) 안에 ${}을 사용하여 자바스크립트 코드를 감싸서 템플릿 리터럴을 사용한다.

function generateLink(image, width){
    const widthInt = parseInt(width, 10);
    return `http://${getProvider()}/${image}?width=${widthInt}`
}

el 태그를 사용해보았기 때문에 더 친숙하게 사용할 수 있을 것같다.

요약

  1. const와 let은 블록 유효범위를 갖는다.
  2. 기본적으로 const를 사용하여 변하지 않는 값을 표현하며, 가변값을 저장할 때는 let을 사용한다. (명시적으로 구분되는 효과도 있다.)
  3. 백틱 (`) 를 사용하여 변수를 읽는 문자열을 사용한다.

2. 배열로 데이터컬렉션을 관리하라.

2장에서는 데이터와, 다른 객체 및 컬렉션을 배열로 변환하는 방법을 살펴본다. 저자가 배열의 사용을 강력추천하기도하고 자료구조의 기본이기도 하니 잘 살펴보는게 좋겠다.

배열의 특징을 다시 한번 상기 시켜보자. 배열은 순서를 갖고 있는 데이터 컬렉션 구조이다.

또, 추가, 제거, 정렬, 필터링, 교체 등 조작이 필요하다면 더욱 적합하다. ES5와 ES6의 새로운 문법 중 상당수가 배열과 관련되어있다.

🔗 array 예제 링크

배열의 이점을 확인할 수 있는 코드는 위 링크를 통해 확인해볼 수 있다.

1.1. 객체를 키-값쌍을 모든 배열로 바꾸기

const dog = {
    name: 'Don',
    color: 'black',
}
dog.name; // Don

위와 같은 객체는

const dogPair = [
    ['name', 'Don'],
    ['color', 'black'],
]
function getName(dog){
    return dog.find(attr => {
        return attr[0] === 'name';
    })[1];
}

처럼 키-값 쌍을 모은 배열로 바꿀 수 있다. 어디까지나 바꿀 수 있다는 예제이므로 참고만 할 것. 현재는 Object.entries()를 이용해서 객체를 키-값 쌍 배열로 변환시킨다.

1.2. includes()를 사용하여 존재 확인하기

배열의 요소의 존재여부를 확인하는 코드를 살펴보자.

const section = ['contact','shipping'];

function displayShipping(sections){
    /* if(sections.indexOf('shipping')){
        return true;
    }
    return false; */ // 위치가 0번째면 false를 리턴하는 불상사가 생김.
    return sections.indexOf('shipping') > -1;   // 위 문제를 개선
}

indexOf()를 사용하여 체크는 예제인데, 문제는 없지만 코드가 직관적이진 않다.

const section = ['contact','shipping'];

function displayShipping(sections){
    return sections.includes('shipping'); // includes() 를 사용.
}

includes()를 사용하여 수정한 코드이다. 코드를 보자마자 이해할 수 있을 정도로 직관적으로 변했다.

1.3. 펼침연산자란?

펼침연산자는 매우 폭넓게 사용되며 유용한 기능이다. 예시를 보면 안쓸 수가 없다. 우선 사용법은 다음과 같다.

const cart = ['Naming and Necessity', 'Alice in Wonderland'];
const copyCart = [...cart]; // ['Naming and Necessity', 'Alice in Wonderland']

펼침연산자의 활용 방법을 구체적으로 보자. 다음은 items 배열에서 removable에 해당하는 요소를 삭제하여 리턴하는 함수다.

function removeItem(items, removable){
    const updated = [];
    for(let i = 0; i < items.length; i++){
        if(items[i] !== removable){
            updated.push(items[i]);
        }
    }
    return updated;
}

문제는 없는 코드인데, 펼침연산자를 사용하면 간단하게 정리할 수 있다.

function removeItem(items, removable){
    const index = items.indexOf(removable);  // 삭제할 요소의 인덱스 추출
    return [...items.slice(0, index), ...items.slice(index + 1)]; // 펼침연산자를 사용하여 slice
}

펼침연산자를 사용하지 않아도 concat()을 사용하면 이어붙여 리턴시킬 수 있지만 다른 사람이 처음 봤을땐 한눈에 이해가 되지 않는다. 펼침연산자를 사용해서 위처럼 리턴시키면 원본 배열에 영향을 주지 않으면서 직관적으로 이해할 수 있다.

펼침연산자의 또 다른 예를 보자.

const book = ['Reasons and Persons', 'Derek Parfit', 19.99];
// 입력한 책의 서식을 만들어주는 함수.
function formatBook(title, author, price){
    return `${title} by ${author} $${price}`;
}
// formatBook(book[0], book[1], book[2]);
formatBook(...book);

기존 방법이었다면 주석처리한 코드처럼 해당 요소를 하나씩 파라미터에 던져줘야할 것을 펼침연산자만 쓰면 한번에 들어간다… 예시를 보니 많이 쓸 수밖에 없을 것 같다.

1.4. 펼침연산자로 원본 데이터 조작을 피하라.

펼침연산자를 사용하는 방법에 대한 예제를 확인해보면 (블로그엔 정리하지 않았지만) splice() 등, 원본 데이터의 조작이 이루어지는 함수를 사용했을때 의도치 않은 결과를 얻게된다. 데이터 원본을 조작하는 일은 언제나 위험하니 피해야할 일이다. 그런데, 배열 메서드들을 보면 원본데이터를 조작하는 놈들이 꽤 많다.

그럴때 펼침연산자를 활용하면 코드를 간결하게 만들면서 문제를 차단할 수 있다.

push() 예제

다음은 장바구니 상품 목록을 받아 요약하는 함수다.

const cart = [{
        name : 'The Foundateion Triology',
        price: 19.99,
        discount: false,
    },
    {
        name : 'Godel, Escher, Bach',
        price: 15.99,
        discount: false,
    },
    {
        name : 'Red Mars',
        price: 5.99,
        discount: true,
    },
];
// 사은품 객체
const reward = {
    name: 'Guide to Science Fiction',
    discount: true,
    price: 0,
};
// 조건을 확인하여 사은품 추가
function addFreeGift(cart){
    if(cart.length > 2){
        //cart.push(reward);
        //return cart;
        return [...cart, reward];   // 펼침 연산자를 사용하여 cart원본을 조작하지 않는다!
    }
    return cart;
}
// 카트 합산
function summarizeCart(cart){
    const cartWithReward = addFreeGift(cart); // addFreeGift 수정 전, 오류가 발생한다...
    const discountable = cart.filter(item => item.discount);
    if(discountable.length > 1){
        return {
            error: '할인 상품은 하나만 주문할 수 있습니다.',
        };
    }
    return {
        discounts: discountable.length,
        items: cartWithReward.length,
        cart: cartWithReward,
    };
}

추가로, 펼침연산자는 컴마(,)로 연결할 수 있다.

sort() 예제

sort()또한 원본의 순서를 조작한다. staff 객체 배열을 정렬하는 코드를 확인하자.

const staff = [
    {
        name: 'Joe',
        years: 10,
    },
    {
        name: 'Theo',
        years: 5,
    },
    {
        name: 'Dyan',
        years: 10,
    },
];
function sortByYears(a, b){
    if(a.years === b.years){
        return 0;
    }
    return a.years - b.years; // 근속연수 오름차순 정렬
}
const sortByName = (a, b) => {
    if(a.name === b.name){
        return 0;
    }
    return a.name > b.name ? 1: -1; // 이름순 오름차순 정렬
}

해당코드를 sort() 메서드를 통해 (1)근속연수 정렬 -> (2)이름순 정렬 -> (3)근속연수 정렬 순서로 실행하게되면, (1)과 (3)의 결과가 다른 것을 확인할 수 있다. 이를 피하기 위해서는 펼침연산자를 사용하여 정렬한다.

[...staff].sort(sortByYears);

이렇게 펼침연산자를 사용하면 이름순 정렬과 근속연수 정렬을 여러번 오가더라도 출력되는 결과값은 동일하다.

요약

  1. 배열은 강력한 데이터 컬렉션이며, 객체는 배열로 변환할 수 있다.
  2. 배열의 요소체크는 includes()로 하라.
  3. 펼침연산자를 활용하면 원본 데이터의 조작을 피할 수 있다.

3. 특수한 컬렉션을 이용해 코드 명료성을 극대화 하라.

모던 자바스크립트에서는 새로 mapset 등의 컬렉션을 지원한다. 자바를 배울때도 사용했던 익숙한 컬렉션이므로 용도에 맞게 적절히 사용하면 굉장히 편리해 질 것이다.

3.1. 키-값 컬렉션으로 사용하는 객체

단순 키-값 탐색 시 객체가 적절한 때가 있다. 책에서는 16진수 색상코드에 키값을 붙이는 예제로 이를 설명한다. 이렇게 중괄호에 키-값을 작성하는 것을 객체 리터럴(object literal)이라고 한다. 객체 리터럴은 정적인 정보에 적합하며, 갱신 및 반복, 대체, 정렬 시에는 맵을 사용하는 것이 적합하다.

function getBill(item){
    return {
        name: item.name,
        due: twoWeeksFromNow(),
        total: calculateTotal(item.price),
    };
}
const bill = getBill({
    name: '객실 청소',
    price: 30
});
// 매개변수를 객체로 받아 값을 꺼내 씀
function displayBill(bill){
    return `${bill.name} 비용은 ${bill.total} 달러이며 납부일은 ${bill.due}입니다.`;
}

추후 해체 할당 관련 예제를 통해 객체를 선택해야하는 이유를 추가로 배울 수 있을 것같다. 현재는 이 정도만 알고 넘어가자.

3.1.1. 객체 복사하기

const defaults = {
    author: '',
    title: '',
    year: 2017,
    rating: null,
};
const book{
    author: 'Joe Morgan',
    title: 'Simplifying JavaScript'
}
// for문을 통한 복사
function addBookDefaults{
    const fields = Object.keys(defaults);   // 키값(필드)을 저장한 배열
    const updated = {};
    for(let i = 0; i < fields.length; i++){
        const field = fields[i];    // 순차적으로 필드명을 참조
        updated[field] = book[field] || defaults[field]; // 해당 필드가 없으면 디폴트를 사용
    }
    return updated;
}
// Object.assign사용!
const updated = Object.assign({}, defaults, book);

// 펼침연산자도 사용할 수 있다. **
const bookWithDefaults = { ...defaults, ...book}

이 방법을 사용하면 일반적인 객체의 값을 복사할 수 있지만, 중첩된 객체의 경우 깊은 복사를 하지 못하기 때문에, 중첩된 객체 또한 Object.assign()을 통해 복사해주어야 하는 점을 잊지 말아야한다.

그게 귀찮으면 Lodash 라이브러리를 찾아 cloneDeep() 메서드를 이용해보면 된다고 한다.

3.2. Map

키-값 쌍이 자주 추가되거나 삭제되는 경우, 키가 문자열이 아닌 경우에는 map을 사용한다.

🔗 map 예제 링크

let items = new Map();

로 선언하며, .set(),.get(),.delete(),.clear() 메서드를 활용해 조작한다.

3.2.1. 맵의 순회

객체 리터럴을 조회하려면 정렬및 순회를 위해 준비할 작업이 많은데, 맵이터레이터에는 순회 기능이 내장되어있으므로 매우 간편하다. 아래코드 처럼 맵을 순회할 수 있다.

const filters = new Map()
    .set('색상', '검정색')
    .set('견종', '래브라도리트리버');

function checkFilters(filters){
    for(const entry in filters){
        console.log(entry);
    }
}
// ['색상', '검정색']
// ['견종', '래브라도리트리버']
// 이터레이터는 키-값 쌍을 넘긴다.

filters.entries();
// MapIterator{ ['색상', '검정색'], ['견종', '래브라도리트리버'] }
// .entries()를 사용하여 키-값 쌍 맵이터레이터를 반환한다.

그런데 맵은 배열처럼 정렬을 할 수 없는 문제가 있는데 이럴 때 펼침연산자를 사용할 수 있다.

function sortByKey(a, b){
    return a[0] > b[0]? 1: -1;
}
function getSortedAppliedFilters(filters){
    const applied = [...filters]    // 맵을 배열로 변환 
        .sort(sortByKey)            // 정렬
        .map([key, value]) => {        // .map()메서드를 활용하여 '키:값' 문자열로 변환
            return `${key}:${value}`;
        })
        .join(', ');                // ,로 연결
    return `선택한 조건은 ${applied} 입니다.`;
}

3.2.1. 맵 데이터 변경하기

필터링 조건을 담은 컬렉션을 만드려고 한다.

const defaults = new Map()
  .set('color', 'brown')
  .set('breed', 'beagle')
  .set('state', 'kansas');

const filters = new Map()
  .set('color', 'black');

function applyDefaults(map, defaults) {
  for (const [key, value] of defaults) {
    if (!map.has(key)) {
      map.set(key, value);
    }
  }
}
export { applyDefaults };

예시 코드의 경우 사용자가 선택한 조건을 알려줄 수 없기 때문에 사본을 만들어 사용한다.

function applyDefaults(map, defaults){
    return new Map([...defaults], [...map]);
}
export { applyDefaults };

위처럼 사용하면 한줄만에 정리되며, 새로운 키로 맵을 생성하면 마지막 선언한 값을 사용하므로 값을 설정하는 대신 갱신하여 사용하게 된다.


3.3. Set

Set은 집합이라는 뜻이다. 집합의 뜻대로 중복값을 허용하지 않고, 고유한 항목의 목록을 생성한다. 이를 사용하면 배열에서 고유한 항목만 분류할 수 있다.

let items = new Set();

로 선언하며, .add(),.has(),.delete(),.clear() 메서드를 활용해 조작한다.

3.3.1. 배열에서 고유항목 분류하기

//...
function getColors(dogs){
    return dog.map(dog => dog['색상']);
}
getColors(dogs); // ['검정색', '검정색', '검정색'];

function getUnique(attributes){
    const unique = [];
    for(const attr of attributes){
        if(!unique.includes(attributes)){
            unique.push(attr);
        }
    }
    return unique;
}

위 코드는 강아지의 색상을 중복없이 얻는 코드이다. 이 때 Set을 활용하면 매우 간단해진다.

function getUniqueColor(dogs){
    const unique = new Set();
    for(const dog of dogs){
        unique.add(dog.색상);
    }
    return [...unique];
}

이 코드는 reduce()를 이용해 더 간결하게 바꿀 수 있는데 그 부분은 다음 시간에…

요약

  1. 객체리터럴은 정적인 정보를 키-값 탐색할때 유용하며, 복사시 Object.assign() 또는 펼침연산자를 사용하면 편리하다.
  2. Map은 키-값 쌍의 변경이 잦거나 키값이 정수일 때 사용한다. 순회시 맵이터레이터를 사용한다.
  3. Set은 고유항목의 목록을 생성할때 사용한다. 특히 배열에서 고유항목을 분류할때 유용하게 쓰인다.

Categories:

Updated:

Comments