JS에서 함수를 잘 활용하는 법
재귀&연결리스트 / 나머지 프로퍼티 & 스프레드문법 & arguments / 전역 객체 / 객체로서 함수 바라보기 & 함수property
- JS에서 재귀 합리적으로 쓰기
1) 재귀란
재귀에 대해 간단한 개념을 알아보고, js에서 어떤 식으로 재귀를 사용하면 효율적인지 알아보자.
재귀 함수가 실행되면, 실행 절차에 대한 정보는 실행 컨텍스트에 저장된다.
따라서 재귀 함수의 중첩 호출이 끝난 뒤에는 함수의 실행 컨텍스트에서 꺼내와 우리가 원하는 실행(계산 등)을 이어나가게 된다.
또한 재귀 깊이는 스택에 들어가는 실행 컨텍스트 수의 최댓값이라고 할 수 있다.
한편, 반복문을 사용하면 대개 함수 호출의 비용(메모리 사용)이 절약되지만
재귀를 사용하면 코드가 짧아지고 코드 이해도가 높아지며 유지보수에도 이점이 있다는 차이점이 있다.
따라서 비용적인 측면과 유지보수적인 측면 등 여러 측면을 고려해 사용한다면 좋을 것이다.
2) 객체 순회에 재귀 알고리즘 사용하기
재귀 알고리즘이 반복문에 비해 유용하게 사용되는 예시는 아래와 같다.
객체 내 하나의 배열은 반복문으로 순회하기에 충분하지만,
만약 객체 내에 객체나 배열이 포함된 복잡한 자료형이 있다면, 재귀를 이용해 값만 순회해오는 게 편리하다.
let company = { // 동일한 객체(간결성을 위해 약간 압축함)
sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600 }],
development: {
sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }],
internals: [{name: 'Jack', salary: 1300}]
}
};
// 급여 합계를 구해주는 함수
function sumSalaries(department) {
if (Array.isArray(department)) { // 첫 번째 경우
return department.reduce((prev, current) => prev + current.salary, 0); // 배열의 요소를 합함
} else { // 두 번째 경우
let sum = 0;
for (let subdep of Object.values(department)) {
sum += sumSalaries(subdep); // 재귀 호출로 각 하위 부서 임직원의 급여 총합을 구함
}
return sum;
}
}
alert(sumSalaries(company)); // 7700
3) 연결리스트 자료형 구현하기
재귀의 개념을 넘어 하나의 자료형을 구축할 수 있다.
재귀적으로 정의된 자료구조는 자기 자신을 이용해서 정의하는데, 이를 연결리스트라고 한다.
연결리스트가 탄생한 배경은 다음과 같다.
배열에서 삭제와 삽입에 있어 많은 양의 비용이 든다. unshit()나 shift() 메서드는 배열의 인덱스 전체를 변경하기 때문이다.
이러한 문제점은 재귀 개념을 이용한 연결리스트로 해결할 수 있다.
연결리스트의 요소는 value와 next (다음 연결 리스트 요소를 참조하는 프로퍼티)로 이루어져 있다.
아래와 같이 next를 참조하며 재배열이 이루어진다. 추가적으로 prev, tail 등의 프로퍼티를 추가해 더 많은 기능을 구현할 수 있다.
let list = { value: 1 };
list.next = { value: 2 };
list.next.next = { value: 3 };
list.next.next.next = { value: 4 };
// list에 새로운 value를 추가합니다.
list = { value: "new item", next: list };
// list의 중간요소를 제거
list.next = list.next.next;
연결 리스트는 배열을 쉽게 재배열할 수 있다는 점에서 장점을 갖지만, 인덱스를 이용해 요소에 쉽게 접근하기는 어렵다는 단점을 가진다.
한 편 한가지에서 뻗어나가는 형태의 자료구조가 재귀를 통해 정의될 수 있음을 확인해볼 수 있었다.
- JS의 함수 속 매개변수 톺아보기
JS에서 매개변수를 잘 활용할 수 있는 방법은 아래와 같이 많다.
하나하나 살펴보며 함수에서 매개변수를 어떻게 효율적으로 사용할 수 있는지 알아보자.
1) 나머지 매개변수
함수의 인수에는 제약이 없다.
다만 배열의 모든 인자를 하나하나 넘겨주기엔 번거로운데, 이때 나머지 매개변수를 사용한다.
나머지 매개변수는 마침표 세 개 ...로 나타내며, "남아있는 매개변수들을 한데 모아 배열에 집어넣어라.”를 의미한다.
나머지 매개변수는 항상 마지막에 있어야 하며, 다른 인자와 함께 사용될 수 있다.
function showName(firstName, lastName, ...titles) {
alert( firstName + ' ' + lastName ); // Bora Lee
// 나머지 인수들은 배열 titles의 요소가 됩니다.
// titles = ["Software Engineer", "Researcher"]
alert( titles[0] ); // Software Engineer
alert( titles[1] ); // Researcher
alert( titles.length ); // 2
}
showName("Bora", "Lee", "Software Engineer", "Researcher");
2) Arguments 객체
유사 배열 객체(array-like object)인 arguments을 활용하면, 매개변수를 인덱스 개념으로 접근할 수 있다.
굳이 매개변수로 인자를 하나하나 정의해야하는 것이 아니기 때문에
마치 함수의 인수 전체가 가상의 배열로 넘어온다고 생각하면 된다.
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );
// arguments는 이터러블 객체이기 때문에
// for(let arg of arguments) alert(arg); 를 사용해 인수를 펼칠 수 있습니다.
}
// 2, Bora, Lee가 출력됨
showName("Bora", "Lee");
// 1, Bora, undefined가 출력됨(두 번째 인수는 없음)
showName("Bora");
하지만 argument는 배열이 아닌 유사 배열 객체이기 때문에, 배열 메서드(map 등)를 사용할 수 없고,
인수의 일부만 사용할 수도 없다!
3) 스프레드문법
배열을 통째로 인수로 넘겨주고 싶을 땐 어떻게 해야할까?
아래와 같이 넘겨주면 함수는 인자를 숫자 목록으로 인지하기 때문에, 배열의 값을 받지 못한다.
따라서 우리는 arr[0], arr[1], arr[2]와 같이 하나하나 인자로 넘겨줘야한다.
let arr = [3, 5, 1];
alert( Math.max(arr) ); // NaN
이때 우리는 스프레드 문법을 사용할 수 있다.
아래와 같이 배열을 하나하나 확장하여 바로 넘겨줄 수 있어 편리하게 사용이 가능하다.
let arr = [3, 5, 1];
alert( Math.max(...arr) ); // 5 (스프레드 문법이 배열을 인수 목록으로 바꿔주었습니다.)
...를 사용하기 때문에 나머지 매개변수와 비슷해 보이지만,
스프레드 문법은 배열 등의 이터러블 객체를 확장하고,
나머지 매개변수는 여러 인자를 하나의 이터러블 객체로 함축해 함수의 매개변수로 저장한다는 측면에서
반대의 역할을 하고 있다고 볼 수 있다.
- 전역 객체를 사용해 전역함수 만들기
전역 객체를 사용하면, 어디서든 접근 가능한 전역변수와 전역 함수를 만들 수 있다.
한 가지 예시로, 전역 객체엔 Array와 같은 내장 객체, window.innerHeight(뷰포트의 높이를 반환함)같은 브라우저 환경 전용 변수 등이 저장되어 있다.
브라우저 환경에선 전역 객체를 window, Node.js 환경에선 global라고 부르는데, 이러한 전역객체엔 프로퍼티를 자유롭게 추가할 수 있다. 이러한 프로퍼티를 전역 변수나 전역 함수로도 지정할 수 있는데, 아래와 같이 var로 전역 변수나 전역 함수를 선언해야만 전역 객체의 프로퍼티가 된다.
var gVar = 5;
alert(window.gVar); // 5 (var로 선언한 변수는 전역 객체 window의 프로퍼티가 됩니다)
- 함수는 객체이다.
함수는 호출이 가능한(callable) '행동 객체’라고 이해할 수 있다. 그렇기 때문에 우리는 함수를 호출 할 수 있을 뿐만 아니라 객체처럼 함수에 프로퍼티를 추가·제거하거나 참조를 통해 전달할 수도 있다. JS의 함수가 제공하는 몇가지 프로퍼티와 기능을 살펴보자.
1) Name 프로퍼티
함수의 이름을 가져올 수 있는 프로퍼티이다.
function sayHi() {
alert("Hi");
}
alert(sayHi.name); // sayHi
2) Length 프로퍼티
함수의 인자 개수를 가져올 수 있는 프로퍼티이다.
이를 통해 아래와 같이 특정 사용자에게 원하는 값에 따라 인자를 다르게 전달해, 해당 인자의 개수에 따라 특정 기능을 만들 수 있다. 또한 인수의 종류에 따라 인수를 다르게 처리하는 방식을 다형성이라고 하는데, 아래의 코드가 프로그래밍 언어의 다형성을 실현한 예시라고 할 수 있다.
아래 코드는 핸들러를 통해 인자를 전달 받는데,
핸들러의 인자의 개수가 없는 경우는 사용자가 OK했을 때 호출되며/
핸들러의 인자의 개수가 있는 경우는 사용자가 OK이든, Cancel이든 호출되는 식으로 이루어진다.
function ask(question, ...handlers) {
let isYes = confirm(question);
for(let handler of handlers) {
if (handler.length == 0) {
if (isYes) handler();
} else {
handler(isYes);
}
}
}
// 사용자가 OK를 클릭한 경우, 핸들러 두 개를 모두 호출함
// 사용자가 Cancel을 클릭한 경우, 두 번째 핸들러만 호출함
ask("질문 있으신가요?", () => alert('OK를 선택하셨습니다.'), result => alert(result));
3) 기명함수 표현식
기명함수 표현식이란 이름이 있는 함수 표현식을 말한다.
기명함수 표현식을 이용해 아래와 같은 2가지 기능을 구현할 수 있다.
- 이름을 사용해 함수 표현식 내부에서 자기 자신을 참조할 수 있습니다.
- 기명 함수 표현식 외부에선 그 이름을 사용할 수 없습니다.
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // 원하는 값이 제대로 출력됩니다.
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Hello, Guest (중첩 호출이 제대로 동작함)
특히 위와 같은 코드에서 진가를 발휘하는데, 5번째 줄에서 func이 아닌 sayHi를 사용했더라면 중간에 초기화된 sayHi함수를 참조하지 못했을 것이다. "func"이라는 이름은 함수 지역 수준(function-local)에 존재하므로 외부 렉시컬 환경에서 찾지 않아도 되고, 내부 함수를 사용한다. 따라서 초기화됨에 상관없이 자기가 포함된 영역의 함수를 호출할 수 있는 것이다.
위의 글을 통해서 아래와 같은 JS의 숨겨진 기능과 개념을 알 수 있었다.
함수의 매개변수를 더 효율적으로 다루는 법
재귀함수를 통해 자료구조를 만들고 더 쉽게 접근하는 법
함수의 내장 프로퍼티를 이용해 객체 관점에서 여러 기능을 활용하는 법
개발을 하면서 함수는 내가 원하는 하나의 기능을 해낸다고만 생각했었는데, 함수라는 객체 자체에도
많은 활용점이 숨겨져 있다는 것을 알게된 시간이었다.
참고자료 :)
https://ko.javascript.info/function-object
'Web > javascript' 카테고리의 다른 글
[javascript] 동기식 언어인 javascript가 통신을 하는 방법 (0) | 2023.06.04 |
---|---|
[javascript] 배열 총정리 (0) | 2023.05.06 |
[JS기본] 객체의 모든 것 / 프로퍼티, 생성자함수, this와 메소드, 옵셔널 체이닝 (1) | 2023.04.23 |
[JS 기본] 함수선언문 VS 함수표현식 / 콜백함수, 화살표함수 친해지기 (0) | 2023.04.10 |
[JS 기본] IF문과 논리연산자에 숨겨진 기능 (0) | 2023.04.10 |