고급 자바스크립트 트릭 10가지 소개

자바스크립트는 개발 프로세스를 더 효율적으로 만들고 코드를 더 깔끔하게 만들 수 있는 많은 숨겨진 기능을 갖춘 다목적 언어입니다. 여러분이 잘 모르지만 코딩 실력을 크게 향상시킬 수 있는 고급 자바스크립트 트릭 10가지를 소개합니다.

1. Aliasing을 사용한 구조 분해

구조조정을 사용하면 배열의 값이나 객체의 속성을 별개의 변수로 압축을 풀 수 있습니다. 에일리어싱(Aliasing)을 사용하면 이 과정에서 변수 이름을 변경할 수 있으므로 API와 같은 외부 소스의 데이터를 다룰 때 특히 유용합니다.

  • 사용 사례: API에서 데이터를 가져올 경우 코드 가독성과 유지보수성을 높이기 위해 프로퍼티에 더 의미 있는 이름을 부여하고 싶을 때.
1
2
3
4
5
6
7
8
9
10
11
const apiResponse = { first_name: 'John', user_age: 30, address: { city: 'New York', zip: '10001' } };
const {
first_name: firstName,
user_age: age,
address: { city: hometown, zip: postalCode },
} = apiResponse;

console.log(firstName); // John
console.log(age); // 30
console.log(hometown); // New York
console.log(postalCode); // 10001

왜 사용하는가? 변수 이름을 보다 명확하고 직관적으로 만들어 코드 가독성과 유지보수를 개선하는 데 도움이 됩니다. 에일리어싱을 사용하면 이름 충돌을 피하고 코드의 명확성을 높여 복잡한 데이터 구조로 작업하기가 더 쉬워집니다.

2. Currying

커링(Currying)은 여러 인수를 받는 함수를 각각 하나의 인수를 받는 일련의 함수로 변환하는 프로세스입니다. 이 기술을 사용하면 보다 유연하고 재사용 가능한 함수를 만들 수 있어 함수형 프로그래밍에 특히 유용할 수 있습니다.

  • 사용 사례: 할인 적용을 위해 재사용 가능하고 구성 가능한 함수를 만듭니다. 서로 다른 할인율에 대해 별도의 함수를 작성하는 대신 하나의 커리 함수를 만들 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
const applyDiscount = (discount) => (price) => price - (price * discount) / 100;
const tenPercentOff = applyDiscount(10);
const twentyPercentOff = applyDiscount(20);

console.log(tenPercentOff(100)); // 90
console.log(twentyPercentOff(100)); // 80

const applyTax = (taxRate) => (price) => price + (price * taxRate) / 100;
const applyTenPercentTax = applyTax(10);

console.log(applyTenPercentTax(100)); // 110
console.log(applyTenPercentTax(twentyPercentOff(100))); // 88

왜 사용하는가? 함수에 인수를 미리 설정할 수 있어 보다 모듈화되고 컴포저블한 코드를 만들 수 있습니다. 재사용 가능성이 높은 유틸리티 함수의 생성을 크게 간소화하여 코드베이스를 더 깔끔하고 유지 관리하기 쉽게 만들 수 있습니다. 커링은 함수를 부분적으로 적용하거나 다른 구성으로 재사용해야 하는 시나리오에서 특히 유용합니다.

3. Debouncing 및 Throttling

디바운싱(Debouncing)과 쓰로틀링(Throttling)은 함수가 실행되는 빈도를 제어하는 기술입니다. 특히 성능을 저하시킬 수 있는 과도한 함수 호출을 방지하기 위해 이벤트 핸들러를 최적화하는 데 유용합니다.

Debouncing

디바운싱은 마지막 호출 이후 일정 시간이 경과할 때까지 함수가 다시 호출되지 않도록 합니다. 이는 검색 입력 필드와 같이 사용자가 입력을 멈출 때까지 기다렸다가 API 호출을 하고자 하는 시나리오에 유용합니다.

  • 사용 사례: 검색 입력 필드를 최적화하여 API 호출 횟수를 줄입니다. 이렇게 하면 사용자가 입력을 마친 후에만 검색을 시작하여 서버 과부하를 방지하고 사용자 환경을 개선할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}

const search = debounce((query) => {
console.log(`Searching for ${query}`);
// Imagine an API call here
}, 300);

document.getElementById('searchInput').addEventListener('input', (event) => {
search(event.target.value);
});

사용자가 트리거 동작을 중단한 후에만 함수가 호출되도록 함으로써 불필요한 함수 호출 횟수를 줄여 성능과 사용자 경험을 개선합니다. 네트워크 요청이나 무거운 계산이 수반되는 작업에 특히 유용합니다.

Throttling

쓰로틀링은 함수가 지정된 기간에 최대 한 번만 호출되도록 합니다. 이는 함수 호출 빈도를 제한하려는 스크롤 이벤트와 같은 시나리오에 유용합니다.

  • 사용 사례: 스크롤 이벤트 처리를 최적화하여 성능을 개선합니다. 이를 통해 브라우저가 너무 많은 이벤트 호출에 압도되는 것을 방지하여 보다 원활하고 반응이 빠른 상호작용을 보장할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function throttle(func, interval) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= interval) {
lastCall = now;
func.apply(this, args);
}
};
}

const handleScroll = throttle(() => {
console.log('Scrolled');
// Imagine complex calculations or DOM updates here
}, 300);

window.addEventListener('scroll', handleScroll);

함수가 제어된 간격으로 호출되도록 하여 성능 문제를 방지하고 브라우저의 부하를 줄이며 더 나은 사용자 경험을 제공합니다. 쓰로틀링은 스크롤 또는 크기 조정 이벤트와 같이 자주 트리거될 수 있는 이벤트 리스너에 특히 유용합니다.

4. Memoization

Memoization은 비용이 많이 드는 함수 호출의 결과를 캐시하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환하는 최적화 기법입니다. 이를 통해 연산량이 많은 함수, 특히 동일한 인수로 자주 호출되는 함수의 성능을 크게 향상시킬 수 있습니다.

  • 사용 사례: 피보나치 계산과 같은 재귀 함수의 성능을 개선합니다. Memoization 기능이 없으면 피보나치 함수를 호출할 때마다 같은 값을 여러 번 중복 계산하게 되어 시간이 기하급수적으로 복잡해집니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const memoize = (fn) => {
const cache = {};
return (...args) => {
const key = JSON.stringify(args);
if (!cache[key]) {
cache[key] = fn(...args);
}
return cache[key];
};
};

const fibonacci = memoize((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(40)); // 102334155

중복 계산을 방지하여 입력이 반복되는 함수의 성능을 크게 향상시킵니다. Memoization은 비효율적이고 반복적인 계산을 관리 가능한 선형 시간 연산으로 전환할 수 있으므로 성능 집약적인 작업을 최적화하는 데 필수적인 기술입니다.

5. Proxy

프록시 객체를 사용하면 다른 객체에 대한 프록시를 생성하여 속성 조회, 할당, 열거, 함수 호출 등과 같은 기본 연산을 가로채고 재정의할 수 있습니다. 이를 통해 객체에 사용자 지정 동작을 추가할 수 있는 강력한 방법을 제공합니다.

  • 사용 사례: 객체 속성 액세스 및 할당에 대한 유효성 검사 및 로깅. 예를 들어, 유형 제약 조건을 적용하고 액세스 시도를 기록하여 더 나은 제어 및 디버깅 기능을 제공할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const user = {
name: 'John',
age: 30,
};

const handler = {
get: (target, prop) => {
console.log(`Getting ${prop}`);
return target[prop];
},
set: (target, prop, value) => {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
return true;
},
};

const proxyUser = new Proxy(user, handler);
console.log(proxyUser.name); // Getting name, John
proxyUser.age = 35; // Setting age to 35
// proxyUser.age = '35'; // Throws TypeError

유효성 검사, 로깅 등과 같은 객체 작업에 대한 사용자 정의 동작을 허용하여 객체 조작에 대한 제어를 강화합니다. 프록시는 액세스 제어 및 데이터 바인딩과 같은 복잡한 로직을 구현하는 데에도 사용할 수 있습니다. 따라서 프록시는 객체의 동작을 관리하고 확장하기 위한 다용도 도구입니다.

6. Generators

제너레이터는 종료했다가 나중에 다시 입력할 수 있는 함수로, 재입력 시에도 컨텍스트와 변수 바인딩을 유지합니다. 제너레이터는 반복기를 구현하고 비동기 작업을 동기식처럼 처리하는 데 유용합니다.

  • 사용 사례: 사용자 지정 객체 탐색을 위한 iterator 구현. 제너레이터는 사용자 정의 반복 동작을 정의하는 간단한 방법을 제공하여 복잡한 데이터 구조를 쉽게 순회할 수 있도록 해줍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* objectEntries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}

const user = { name: 'John', age: 30, city: 'New York' };

for (let [key, value] of objectEntries(user)) {
console.log(`${key}: ${value}`);
}

// name: John
// age: 30
// city: New York

제너레이터를 사용하면 복잡한 반복 로직과 비동기 프로세스를 보다 쉽게 처리할 수 있어 코드 가독성과 유지 관리가 용이해집니다. 또한 co와 같은 라이브러리를 사용하여 보다 간단하고 선형적인 방식으로 비동기 작업을 관리하는 등의 작업에도 사용할 수 있습니다.

7. Console 활용하기

  • 사용 사례: 복잡한 객체 디버깅을 위한 로깅 개선. console.table, console.group, console.time과 같은 콘솔 메서드는 보다 체계적이고 유익한 디버그 정보를 제공할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Basic logging
console.log('Simple log');
console.error('This is an error');
console.warn('This is a warning');

// Logging tabular data
const users = [
{ name: 'John', age: 30, city: 'New York' },
{ name: 'Jane', age: 25, city: 'San Francisco' },
];
console.table(users);

// Grouping logs
console.group('User Details');
console.log('User 1: John');
console.log('User 2: Jane');
console.groupEnd();

// Timing code execution
console.time('Timer');
for (let i = 0; i < 1000000; i++) {
// Some heavy computation
}
console.timeEnd('Timer');

디버깅 정보의 가시성과 정리가 향상되어 문제를 더 쉽게 진단하고 해결할 수 있습니다. 콘솔 방법을 올바르게 사용하면 명확하고 체계적이며 상세한 로그를 제공함으로써 디버깅 프로세스의 효율성을 크게 향상시킬 수 있습니다.

8. structuredClone을 사용한 구조화된 복제

새로운 structuredClone을 사용하여 오브젝트를 Deep Clone합니다. 기존의 얕은 복사본과 달리 구조화된 복제는 개체의 깊은 복사본을 만들어 중첩된 개체도 복사되도록 합니다. 이 방법은 함수, 정의되지 않은 참조, 순환 참조와 같은 특정 데이터 유형을 처리할 수 없는 JSON.parse(JSON.stringify(obj))의 한계를 피할 수 있습니다.

  • 사용 사례: 복잡한 객체의 심층 사본 생성. 원본 데이터를 변경해서는 안 되는 작업을 위해 객체를 복제해야 할 때 유용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {
a: 1,
b: { c: 2 },
date: new Date(),
arr: [1, 2, 3],
nestedArr: [{ d: 4 }],
};
const clonedObj = structuredClone(obj);

console.log(clonedObj);
// { a: 1, b: { c: 2 }, date: 2024-09-20T00:00:00.000Z, arr: [1, 2, 3], nestedArr: [{ d: 4 }] }
console.log(clonedObj === obj); // false
console.log(clonedObj.b === obj.b); // false
console.log(clonedObj.date === obj.date); // false
console.log(clonedObj.arr === obj.arr); // false
console.log(clonedObj.nestedArr[0] === obj.nestedArr[0]); // false

이 메서드는 JSON.parse(JSON.stringify(obj))와 같은 대안보다 더 안정적이며 복잡한 데이터 구조를 더 잘 처리합니다.

9. 자체 호출(Self-Invoking) 함수

즉시 호출 함수 표현식(Immediately Invoked Function Expressions, IIFE)이라고도 하는 자체 호출(Self-Invoking) 함수는 생성된 후 자동으로 실행됩니다. 코드를 캡슐화하여 전역 범위를 오염시키지 않도록 하는 데 유용하며, 이는 깔끔하고 모듈화된 코드를 유지하는 데 필수적입니다.

  • 사용 사례: 코드를 캡슐화하여 전역 범위의 오염을 방지합니다. 이 기술은 블록 범위(let 및 const)를 사용할 수 없는 구형 JavaScript 환경이나 초기화 로직에 즉각적인 실행이 필요한 시나리오에서 특히 유용합니다.
1
2
3
4
5
6
7
8
(function () {
const privateVar = 'This is private';
console.log('Self-invoking function runs immediately');
// Initialization code here
})();

// Private variables are not accessible from outside
// console.log(privateVar); // ReferenceError: privateVar is not defined

전역 변수를 피하고 전역 범위에 흔적을 남기지 않고 초기화 코드를 실행하여 깔끔한 코드를 유지하는 데 도움이 됩니다. 이 접근 방식은 대규모 코드베이스에서 충돌을 방지하고 기능을 더 잘 캡슐화하여 코드 유지보수성을 개선하고 부작용을 방지할 수 있습니다.

10. Tagged Template Literals

국제화, HTML 위생 처리 또는 동적 SQL 쿼리 생성 등 특수 템플릿을 만들 때 유용합니다.

  • 사용 사례: HTML 템플릿에서 사용자 입력을 위생 처리하여 XSS 공격을 방지합니다. 이 기술을 사용하면 악성 스크립트를 실행하지 않고도 사용자가 생성한 콘텐츠를 DOM에 안전하게 삽입할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function sanitize(strings, ...values) {
return strings.reduce((result, string, i) => {
let value = values[i - 1];
if (typeof value === 'string') {
value = value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
return result + value + string;
});
}

const userInput = '<script>alert("xss")</script>';
const message = sanitize`User input: ${userInput}`;
console.log(message); // User input: &lt;script&gt;alert("xss")&lt;/script&gt;

Template Literal의 출력을 제어하고 사용자 정의할 수 있는 강력한 메커니즘을 제공하여 보다 안전하고 유연한 템플릿 생성을 가능하게 합니다. 보안을 강화하고, 문자열 형식을 지정하고, 동적 콘텐츠를 생성하여 코드의 견고성과 다양성을 향상시킬 수 있습니다.

결론

자바스크립트는 더 깔끔하고 효율적인 코드를 작성하는 데 도움이 되는 기능이 풍부한 언어입니다. 이러한 고급 트릭을 코딩 작업에 통합하면 생산성을 향상하고 코드의 가독성을 높일 수 있습니다.

Share