JAVASCRIPT
코드 품질을 높여주는 JS 함수
🔎 Debounce
debounce 함수는 일련의 빠른 이벤트가 반복적으로 함수를 활성화 하는 것을 방지하는 역할을 한다. 이벤트가 실행되지 않은 채로 일정 시간이 경과할 때까지 이벤트를 실행시키지 않고 함수의 실행을 연기하는 방식으로 동작한다. debounce 함수는 사용자가 버튼을 빠르게 클릭했을 때 함수들이 실행되는 것을 방지하여 성능을 향상하는 방식으로 아주 유용한 해결책이다.
// JS에서 debounce 구현
function debounce(func, delay) {
let timeout
return function() {
const context = this
const args = arguments
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(context, args), delay)
}
}
위 코드는 실제 JS에서 debounce를 어떻게 구현하는지 보여주는 코드로 debounce 함수는 호출 시 전달받은 delay만큼 func을 지연시켜 실행하는 새로운 함수를 반환하게 된다. 만약 함수가 다시 호출된다면, timeout은 초기화 되며 func의 호출은 다시 연기된다.
// 레이아웃을 업데이트하는 함수 정의
function updateLayout() {
// 레이아웃 업데이트...
}
// 디바운스된 함수를 생성
const debouncedUpdateLayout = debounce(updateLayout, 250)
// 윈도우의 리사이즈 이벤트를 받아 디바운스된 함수를 호출
window.addEventListener("resize", debouncedUpdateLayout)
위 코드는 실제 사례에서 debounce 함수가 어떻게 사용되는지 보여준다. 윈도우 창 크기가 변경될 때 updateLayout 함수는 250ms마다 한 번씩만 호출된다. 이를 통해 웹페이지의 효율성과 반응성이 향상된다.
.
🔎 Throttle
throttle 함수는 debounce 함수와 비슷하지만 동작에 약간의 차이가 있다. debounce 함수는 특정 함수가 호출되는 속도를 제한하지만, throttle 함수는 특정 함수가 실행되는 속도를 제한하는 것으로, 특정 기간 내에 어떤 함수를 이미 호출했다면 그 함수가 실행되는 것을 막는 역할을 한다.
// JS에서 throttle 구현
function throttle(func, delay) {
let wait = false
return (...args) => {
if (wait) {
return
}
func(...args)
wait = true
setTimeout(() => {
wait = false;
}, delay)
}
}
위 코드는 실제 JS에서 throttle을 어떻게 구현하는지 보여주는 코드로 throttle 함수는 주어진 func 를 실행하고, wait 변수를 true로 갱신한 후에 타이머를 시작한다. 이 타이머는 delay 만큼의 시간이 흐르면 wait을 다시 초기화한다. 만약 throttle 함수가 다시 호출된다면, wait 을 확인하여 true 라면 그냥 리턴하고 아니라면 주어진 함수를 다시 호출한다.
// 레이아웃을 업데이트하는 함수 정의
function updateLayout() {
// 레이아웃 업데이트...
}
// 함수의 스로틀된 버전 생성
const throttledUpdateLayout = throttle(updateLayout, 250)
// 윈도우 창의 스크롤 이벤트가 발생하면 스로틀된 함수를 실행
window.addEventListener("scroll", throttledUpdateLayout)
위 코드는 실제 사례에서 trottle 함수가 어떻게 쓰이는지 보여준다. 윈도우 창이 스크롤 될 때 updateLayout 함수는 250ms 마다 한 번씩만 실행된다. 지연 시간이 끝나기 전에 이벤트가 발생하면 아무 일도 일어나지 않는다.
.
🔎 Once
once 함수는 이미 호출된 함수가 다시 실행되지 않도록 하는 메서드이다. 이 메서드는 특히 이벤트 리스너를 이용하여 작업하는 동안, 오직 한 번만 실행해야 하는 함수가 자주 있는 경우 유용하게 사용할 수 있다. 매번 이벤트 리스너를 제거하는 대신 once 함수를 사용할 수 있다.
// JS에서 once 구현
function once(func) {
let ran = false
let result
return function() {
if (ran) return result
result = func.apply(this, arguments)
ran = true
return result
}
}
위 코드는 실제 JS에서 once 함수를 어떻게 구현하는지 보여주는 코드로 최초로 once로 func 이 호출될 경우 ran 변수가 true로 설정되어 다시는 func이 호출되지 않는다.
// 요청을 보내는 함수 정의
function requestSomeData() {
// 요청을 보냄
}
// 한 번만 호출될 수 있는 requestSomeData 생성
const sendRequestOnce = once(sendRequest)
// 버튼의 클릭 이벤트가 발생하면 "once" 함수를 호출
const button = document.querySelector("button")
button.addEventListener("click", sendRequestOnce)
위 코드는 실제 사례에서 once 함수가 어떻게 쓰이는지 보여준다. 원래 1번만 호출되게 하기 위해선 button의 click 이벤트가 발생한 순간 removeEventListener로 클릭 리스너를 제거해야하지만 once 함수를 사용하여 간단하게 구현한다.
.
🔎 Memoize
memoize 함수는 동일한 인수로 연산 비용이 많이 드는 루틴을 여러 번 호출하는 것을 방지하기 위해 특정 함수의 결과를 캐싱하는 용도로 사용한다.
// JS에서 memoize 구현
function memoize(func) {
const cache = new Map()
return function() {
const key = JSON.stringify(arguments);
if (cache.has(key)) {
return cache.get(key)
}
const result = func.apply(this, arguments)
cache.set(key, result)
return result
}
}
위 코드는 실제 JS에서 memoize 함수를 어떻게 구현하는지 보여주는 코드로 주어진 func에 결과를 캐싱하고 동일한 인수로 다시 호출될 때 그 결과 값을 가져오기 위해 인수를 키로 사용한다. 복잡합 계산을 수행하는 함수가 있다면, 결과 값을 캐싱하고 동일한 입력값으로 여러 번 호출될 때 즉시 값을 가져올 수 있도록 사용할 수 있다.
// 계산을 수행하는 함수 정의
function fibonacci(n) {
if (n < 2)
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
}
// 메모화된 함수 생성
const memoizedFibonacci = memoize(fibonacci)
// 여러 입력 값으로 메모화된 함수 호출
console.time('total')
console.time('sub1')
const result1 = memoizedFibonacci(30)
console.timeEnd('sub1')
console.time('sub2')
const result2 = memoizedFibonacci(29)
console.timeEnd('sub2')
console.time('sub3')
const result3 = memoizedFibonacci(30)
console.timeEnd('sub3')
console.timeEnd('total')
/*
console 출력값
sub1: 20.422119140625 ms
sub2: 11.713134765625 ms
sub3: 0.016845703125 ms
total: 32.51416015625 ms
*/
위 코드는 피보나치 수 계산에서 memoized 함수를 사용하는 예시이다. 두 번째 호출인 29에 대한 피보나치 수를 계산함에도 30에 대한 피보나치 수를 계산하는 세 번째 호출보다 더 오래걸리는 것을 볼 수 있다.
.
🔎 Curry
curry 함수는 일부 인수를 '미리 채움'으로써 이미 존재하는 함수에서 새로운 함수를 만드는 데 사용되는 고급 자바스크립트 함수이다. 커링은 여러 인수를 받는 함수로 작업할 때 자주 사용되는데, 항상 같을 인수들을 제외한 나머지 인수만을 받는 함수로 변환한다.
// JS에서 curry 구현
function curry(func, arity = func.length) {
return function curried(...args) {
if (args.length >= arity) return func(...args)
return function(...moreArgs) {
return curried(...args, ...moreArgs)
}
}
}
위 코드는 실제 JS에서 curry 함수를 어떻게 구현하는지 보여준다. curry는 func를 인수로 받고 func의 인수의 길이를 기본값으로 가지는 arity 인수를 선택적으로 가진다. curry는 arity 수만큼의 인수와 함께 호출되는 새로운 curried 함수를 반환한다. 모든 인수가 제공되지 않은 경우, 필요한 인수가 모두 주어질 때까지 더 많은 인수로 호출할 수 있는 새로운 함수를 반환하고, 모든 인수가 주어지면 func가 호출되어 결과 값을 반환한다.
// 두 점 간의 거리를 계산하는 함수 정의
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
}
// 한 점만 필요로 하게끔 함수를 curry된 버전으로 생성
const distanceFromOrigin = curry(distance, 4)(0, 0)
// 다른 점을 인수로 하여 curry된 함수를 호출
const d1 = distanceFromOrigin(1, 1)
const d2 = distanceFromOrigin(2, 2)
위 코드는 실제 사례에서 cuury 함수를 사용하는 예시이다. curry를 통해 x1, y1 (0, 0)을 이미 가지고 있는 커리 된 함수(distanceFromOrigin)가 생성되고 이 후에는 나머지 x2, y2 인수만을 받아 원래 distance 함수의 결과가 반환된다.
.
🔎 Partial
JS에서 partial 함수는 curry 함수와 유사하다. 하지만 curry 함수는 커링 체인에서 다른 함수를 반환하고, partial 함수는 결과를 즉시 반환한다는 차이가 있다.
function partial(func, ...args) {
return function partiallyApplied(...moreArgs) {
return func(...args, ...moreArgs)
}
}
위 코드는 실제 JS에서 partial 함수를 어떻게 구현하는지 보여준다. partial 함수는 일반적으로 함수와 하나 이상의 입력 인수를 받고, 새로운 함수가 호출될 때 인수로 받은 함수에 추가로 받은 인수들을 전달하여 호출하는 새로운 함수를 반환한다.
// 계산하는 함수 정의
function calculate(x, y, z) {
return (x + y) * z
}
// 마지막 인수(z)만 필요로 하도록 parital이 적용된 함수 버전으로 생성
const multiply10By = partial(calculate, 8, 2)
// 반복 횟수 값을 전달하여 partial이 적용된 함수를 호출
const result = multiply10By(5)
위 코드는 실제 사례에서 partial 함수를 사용하는 예시이다. calculate 함수에 미리 8과 2라는 첫 2개의 인수를 채워서 multiply10By 라는 새로운 함수를 partial 함수를 통해 생성한다. multiply10By는 10에 곱할 숫자 1개만을 인수로 필요로 한다.
.
🔎 Pipe
pipe 함수는 여러 개의 함수를 연결하고 그 연결고리에서 어떤 함수의 결과를 다음 함수로 전달하고 싶을 때 사용하는 유틸리티 함수이다.
function pipe(...funcs) {
return function piped(...args) {
return funcs.reduce((result, func) => [func.call(this, ...result)], args)[0]
}
}
위 코드는 실제 JS에서 pipe 함수를 어떻게 구현하는지 보여준다. Unix pipe 연산자와 유사하며 JS의 reduce 함수를 사용하여 모든 함수를 왼쪽에서 오른쪽으로 적용한다.
// 문자열에 추가하는 함수들 정의
function addPrefix(str) {
return "prefix-" + str
}
function addSuffix(str) {
return str + "-suffix"
}
function toUppercase(str) {
return str.toUpperCase()
}
// 세 가지 함수를 올바른 순서로 적용한 파이프된 함수를 생성
const decorated1 = pipe(addPrefix, addSuffix, toUppercase)
const decorated2 = pipe(toUppercase, addPrefix, addSuffix)
// 입력 문자열로 파이프된 함수 호출
const result1 = decorated1("hello") // PREFIX-HELLO-SUFFIX
const result2 = decorated2("hello") // prefix-HELLO-suffix
위 코드는 실제 사례에서 pipe 함수가 어떻게 사용되는지 보여준다. addPrefix, addSuffix, toUppercase 함수들을 각각 다른 순서로 pipe 함수를 사용하였는고 이에 따라 결과가 달라지는 것을 볼 수 있다. (왼쪽에서 오른쪽 순서로 적용)
.
🔎 Compose
compose 함수는 pipe 함수와 동일하지만 reduceRight을 사용하여 주어진 함수들을 적용하기 때문에 함수들이 오른쪽에서 왼쪽으로 적용된다.
function compose(...funcs) {
return function composed(...args) {
return funcs.reduceRight((result, func) => [func.call(this, ...result)], args)[0]
}
}
.
🔎 Pick
pick 함수는 객체에서 특정한 값들을 선택하는 목적으로 사용된다. 주어진 객체에서 특정 프로퍼티들을 선택해서 새로운 객체를 생성한다.
function pick(obj, keys) {
return keys.reduce((acc, key) => {
if (obj.hasOwnProperty(key)) {
acc[key] = obj[key]
}
return acc
}, {})
}
위 코드는 실제 JS에서 pick 함수를 어떻게 구현하는지 보여준다. 새로운 객체로 선택할 키의 배열(keys) 를 reduce를 통해 반복하며 해당 key 가 obj에 있을 경우 {} 로 초기화된 acc에 key값과 데이터를 넣어 누산하여 리턴해주면 keys의 key만을 가지는 객체를 얻을 수 있다.
const obj = {
id: 1,
name: 'Paul',
password: '82ada72easd7',
role: 'admin',
website: 'https://www.paulsblog.dev',
}
const selected = pick(obj, ['name', 'website'])
console.log(selected) // { name: 'Paul', website: 'https://www.paulsblog.dev' }
위 코드는 실제 사례에서 pick 함수가 어떻게 사용되는지 보여준다.
.
🔎 Omit
omit 함수는 pick 함수와 정반대이다. 어떤 객체에서 특정한 프로퍼티들을 제거하고 싶을 때 사용한다.
function omit(obj, keys) {
return Object.keys(obj)
.filter(key => !keys.includes(key))
.reduce((acc, key) => {
acc[key] = obj[key]
return acc
}, {})
}
.
🔎 Zip
zip 함수는 전달된 요소들의 배열을 다른 배열의 요소와 결합하는 JS 함수이며, 여러 배열을 하나의 튜플 배열로 결합하는 데 사용된다. 결과 배열에는 각 배열에서 상응하는 요소들이 포함된다. 이러한 기능은 어떠한 방식으로든 병합하거나 연관시켜야 하는 여러 소스의 데이터로 작업할 때 자주 사용된다.
function zip(...arrays) {
const maxLength = Math.max(...arrays.map(array => array.length))
return Array.from({ length: maxLength }).map((_, i) => {
return Array.from({ length: arrays.length }, (_, j) => arrays[j][i])
})
}
위 코드는 실제 JS에서 zip 함수가 어떻게 구현되는지 보여준다. 원래 배열의 모든 요소는 또 다른 배열에서 동일한 인덱스를 가지는 요소와 매핑된다.
// 좌표를 포함하는 세 배열을 정의
const xCoordinates = [1, 2, 3, 4]
const yCoordinates = [5, 6, 7, 8]
const zCoordinates = [3, 6, 1, 7]
// 각 좌표의 배열을 zip된 배열로 만들기
const points = zip(xCoordinates, yCoordinates, zCoordinates)
// 좌표들의 zip된 배열 사용
console.log(points) // [[1, 5, 3], [2, 6, 6], [3, 7, 1], [4, 8, 7]]
위 코드는 실제 사례에서 zip 함수가 어떻게 사용되는지 보여준다. x, y, z 좌표를 튜플로 이루어진 한 배열로 결합시킨다.
.
🔎 마무리
위 글에서 소개한 함수들은 기본 JS에 내장되어 있지는 않다. 하지만 underscore.js, lodash와 같이 널리 사용되는 JS 프레임워크에 구현되어 있다. 이러한 함수들을 올바르게 적용한다면, 코드 품질을 더욱 향상시킬 수 있고, 프로젝트 작업을 더 쉽게 만들어준다.
.
참조
https://velog.io/@typo/advanced-javascript-functions-to-improve-code-quality