💡 이번에 배운 내용
- Section1.
웹 프론트엔드 개발의 기초지식을 기반으로 스스로 단순한 Web App을 만들 수 있다.- Unit10. Javascript 핵심 개념과 문법: javascript 원시 자료형과 참조 자료형, 스코프, 클로저, ES6 신규 문법 등에 대해 배운다.
정말 중요한 핵심 개념들을 배웠다. 특히 클로저가 많이 어렵다고 느껴서 원문도 뒤져보고 문제도 풀어보고 정말 다방면으로 이해를 시도했다. 그래도 이것저것 해보니까 학습을 어느 정도 진행한 지금은 좀 낫다. 특히 학습 후 진행한 과제가 이해에 큰 도움이 되었다.
이번에도 배운 내용을 정리하고, 과제 후 과제 리뷰를 하면서 정리하고......
이렇게 정리의 반복으로 점점 머리에 차곡차곡 지식과 경험이 쌓여가는 느낌이 든다.
다만 부작용으로 머리에서 김이 나는 것 같다. 정수리가 아직도 뜨겁다.🤯
원시 자료형, 참조 자료형, 전역 스코프, 지역 스코프, 함수 스코프, 클로저, 내부 함수, 외부 함수, lexical environment, 전개 구문, 나머지 매개변수, 구조 분해 할당
데이터를 저장하는 방식에 따라 아래와 같이 type을 나눌 수 있다.
원시 자료형은 변수 이름을 값 자체를 저장하고,
참조 자료형은 데이터가 보관된 주소를 저장한다.
그래서 아래와 같은 상황이 발생한다.
원시 자료형 데이터는 각각 변수에 담고, 그 변수끼리 복사하면
데이터 값이 복사되기 때문에 원래의 변수에 영향이 가지 않는다.
let first=1;
let second=first;
second=2;
console.log(first, second); //1, 2
때문에 아래 예제에서 함수를 실행해도
값을 복사하여 사용했기 때문에 something의 원래 값은 바뀌지 않는다.
let something=1;
function changeValue(value){
value=value-1;
}
changeValue(something);
console.log(something);//1
참조 자료형 데이터는 각각 변수에 담고, 그 변수끼리 복사하면
그 데이터가 담긴 주소를 복사하기 때문에 원래의 변수에 영향이 간다.
데이터를 복사한게 아니라 데이터가 보관된 주소를 복사했기 때문이다.
let first=[1,2,3,4];
let second=first;
second[0]=5;
console.log(first, second); //[5,2,3,4] [5,2,3,4]
console.log(first===second); //true
console.log([]===[]); //false
위 코드의 마지막 두 줄을 보면 알 수 있듯이 배열 안의 데이터 값이 같은데 어떤 건 true가 어떤 건 false가 나온다.
왜냐하면 참조 자료형은 변수에 주소를 할당하고, 비교시 저장된 주소를 비교하기 때문이다.
따라서 마지막 []===[]는 각각 다른 주소로 저장되었기 때문에 주소가 다르므로 같지 않다.
원시 자료형의 예제와 비슷한(?) 아래 예제를 확인해보면
let something=[1,2,3];
function changeValue(value){
value=value.pop();
}
changeValue(something);
console.log(something);//[1,2]
함수 실행시 값을 복사하여 실행했어도 something의 값이 바뀜을 확인할 수 있다. 같은 데이터를 참조하는 주소를 복사했기 때문이다.
(만약 somgthing이 바뀌질 않길 바란다면 배열의 경우 slice()를 이용해 값만 복사할 수 있다.)
타입과 자료구조 참고: javascript MDN - 타입과 자료구조
javascript에서 스코프는 변수의 범위를 의미한다.
범위를 나누는 기준은 보통 중괄호({}) 또는 함수이다.
1) let으로 안쪽에서 선언한 변수는 바깥쪽에서 사용이 불가능하다.
2) 바깥쪽에서 선언한 변수는 안에서도 사용이 가능하다.
3) 스코프는 중첩이 가능하며 마찬가지로 안에서 let으로 선언한 변수는 그 바깥에서 사용하지 못한다.
4) 지역 변수는 전역 변수보다 더 높은 우선순위를 가진다.
가장 바깥쪽의 스코프는 전역 스코프(Global Scope)라고 한다.
그 반대는 지역 스코프(Local Scope)라고 한다.
만약 범위를 벗어나서 let으로 변수를 선언하면 콘솔 창에
Uncaught ReferenceError: 변수 is not defined
가 뜬다.
let descartes="나는 존재한다.";
function checkScope(value){
let thinking="나는 생각한다, 고로 "
console.log(thinking+value);
}
checkScope(descartes);//나는 생각한다, 고로 나는 존재한다.
console.log(thinking+descartes);//ReferenceError
console.log(thinking);//ReferenceError
console.log(descartes);//나는 존재한다.
위의 코드를 보면 외부에서 데카르트를 호출했을 때
그는 존재하나 생각은 에러임을 알 수 있다.
그리고 위 코드의 전체 영역은 바깥쪽 스코프(전역 스코프),
checkScope함수 안의 영역은 안쪽 스코프라고 한다.
바깥쪽 스코프에서 사용한 변수는 안에서도 사용할 수 있으며,
안쪽 스코프에서 사용한 변수는 안에서만 사용할 수 있고 바깥쪽에서는 사용할 수 없다.
전역 스코프와 안쪽 스코프에서 같은 이름의 변수를 선언하면,
안쪽 스코프에서 우선순위는 안쪽에서 선언한 변수이다.
let best="나는 김전역, 내가 제일 잘 나가"
function whoIsTheBest(){
let best="나는 나지역, 내가 제일 잘 나가"
console.log(`1. 누가 제일 잘 나가? : ${best}`);
//1. 누가 제일 잘 나가? : 나는 나지역, 내가 제일 잘 나가
}
console.log(`2. 누가 제일 잘 나가? : ${best}`);
//2. 누가 제일 잘 나가? : 나는 김전역, 내가 제일 잘 나가
whoIsTheBest();
console.log(`3. 누가 제일 잘 나가? : ${best}`);
//3. 누가 제일 잘 나가? : 나는 김전역, 내가 제일 잘 나가
위의 코드를 보면 나지역은 whoIsTheBest 안에서만 잘 나가는 것을 확인할 수 있다.
만약 안쪽에서 선언하지 않고 재할당만 했다면 전역으로 선언한 best의 값이 함수 안에서 재할당 값으로 바뀌게 된다.
1), 2)에서 예제로 살펴봤듯이 변수 선언에 따라 그 사용이 영향을 받을 때 이 범위(스코프)를 유효범위라고 한다.
위에서 let으로 변수를 선언할 때 유효범위는 함수 스코프였다. 이외에 let은 유효범위로 블록스코프도 있다. 즉 let은 스코프의 특징에서 언급한 것처럼 블록스코프의 영향을 받는다.
이외에도 블록 스코프, 함수 스코프에는 여러 차이가 있으니 그건 나중에 학습하며 좀 더 알아볼 예정이다.
변수를 정의하는 명령어에는 let, const, var 등이 있다.
(영어를 좀 하시는 분이라면 클로저는 한글 설명 뿐만 아니라 영문 설명도 보시기를 권장드린다.)
참고 링크의 한글로 '클로저는 함수와 함수가 선언된 어휘적 환경(lexical environment)의 조합이다.'
영어로 '클로저는 클로저를 둘러싼 상태에 대한 참조들로 구성된, 함수의 조합이다.'
라고 쓰여있다.
정의에 얽매이면 이해도 어렵고 활용에 어려움이 있으므로
아래의 예제를 보면서 이해하는 것이 좋다.
const multiple=function(a){ //외부 함수
return function(b){ //내부 함수
return a*b;
}
}
let multiple3=multiple(3);
multiple3(2);//6
예제의 내부함수에서 외부함수 스코프에 접근할 수 있으며 외부함수가 한 번 생성되면 내부함수는 매번 실행 가능하다.
좀 더 직관적으로 이야기하자면 외부함수 안에 리턴되는 함수가 클로저 함수다.
그리고 mutiple3에서 외부함수가 한 번 실행될 때 외부 함수의 인자는 저장되고, 외부함수는 종료되었지만 그 안의 내부함수는 계속 사용할 수 있다. 이를 직관적으로 표현하면 내부함수를 둘러싼 외부함수를 어휘적 환경이라고 한다.
cf. 왜 외부함수는 클로저 함수가 아닌가? 그렇게 되면 모든 함수가 클로저 함수가 되기 때문이다! 위 예제처럼 특수한 경우를 클로저 함수라고 구분하기 위해 보통 실무에서는 내부함수를 클로저 함수라고 부른다고 한다.
클로저 함수는 일반 함수와는 다르게 외부 함수의 실행이 끝나도 '어휘적 환경'을 저장한다. 즉 내부 함수가 저장된다.
때문에 아래와 같이 함수를 계속 응용하여 사용할 수 있다.
const multiple=function(a){ //외부 함수
return function(b){ //내부 함수
return a*b;
}
}
let myMultiple=multiple(2); //내부함수, a(값 2)가 저장된다.
myMultiple(1); //2 -> a*b는 2*1과 같다.
myMultiple(2); //4 -> a*b는 2*2과 같다.
myMultiple(3); //6 -> a*b는 2*3과 같다.
myMultiple(4); //8 -> a*b는 2*4과 같다.
위 방법을 활용해 HTML태그 생성기 등을 만들 수 있으며
클로저 모듈 패턴을 만들 수 있다.
클로저 모듈 패턴 예제: 구구단 확인하기
좋은 예제라고 할 수는 없지만 클로저 모듈 패턴을 이해하는 데 도움은 된다.
const chkMultipleTable=()=>{
let times=1;
return {
nextTimes: ()=>{
times=times+1;
},
prevTimes: ()=>{
if(times>1) times=times-1;
},
chkTimes: ()=> times,
mutiple: (value)=>{
console.log(`${times}단 중 ${times}X${value}=${times*value}`);
}
}
}
let times2=chkMultipleTable();
times2.nextTimes();
times2.chkTimes(); //2
times2.mutiple(3); //'2단 중 2X3=6'
위 구구단 확인하기 예제를 참고하자면 함수 안의 times는 전역변수가 아니기에 부수 효과를 줄일 수 있다.
또한 times값은 클로저를 이용해 변형할 수 있지만 직접적으로 수정할 수 없으므로 안전하게 사용이 가능하다. 또한 내부 함수로 접근하는 것을 막으면서 내부 함수를 사용할 수 있으므로 이름 그대로 폐쇄(클로저. closure)에 유리하다.
-> 이렇게 클로저를 사용해 내부함수의 접근을 막으면서 저장된 변수도 안전하게 사용하는 것을 캡슐화라고 한다.
또한 let times3=chkMultipleTable()를 생성하여 times2처럼 활용이 가능하며 times2에 영향을 주지 않고 사용할 수 있다.
-> 이렇게 하나의 함수를 활용하며 재사용하고 독립적으로 사용하는 것을 모듈화라고 한다.
물론 이처럼 메모리상에 외부함수의 변수와 내부함수가 계속 남아있기 때문에 클로저를 남발해서는 안된다.
정리하자면
클로저의 장점은 아래와 같다.
클로저의 단점은 아래와 같다.
그동안 ES6문법을 아예 사용하지 않은 것은 아니지만,
좀 더 알아보면 좋은 내용들을 정리했다.
함수를 실행할 때 인자가 배열일 경우, 하나의 요소씩 모두 인자로 자동 전달해준다. 이름 그대로 spread하게 요소를 펼쳐 넣어주며 배열 채로 넣지 않는다.
function sumAll(x,y,z){
return x+y+z;
}
let myArr=[1,2,3];
//전개 구문 사용
sumAll(...myArr);//6
전개 구문은 배열을 합치거나 복사하는데도 활용 가능하다.(immutable)
때문에 원본 배열, 객체가 바뀌지 않는다.
객체에도 복사하거나 합치는 게 가능한데, 키 값이 같으면 나중에 합친걸로 교체된다.
let myArr=[1,2];
let arr=[3,4];
myArr=[...myArr, ...arr];//[1,2,3,4]
let maxValue=Math.max(arr);//NaN
maxValue=Math.max(...arr);//4
let myObj1={ id:1, name:'bob', age:19 };
let myObj2={ id:2, name:'bobby'};
let mergeMyObj={...myObj1, ...myObj2};//{id: 2, name: 'bobby', age: 19}
function myFunc(x,y, ...args){
console.log(x);
console.log(y);
console.log(args);
}
myFunc(1,2,3,4,5,6);
/*
console.log(x); -> 1
console.log(y); -> 2
console.log(args); -> [3,4,5,6]
*/
전개 구문과 비슷해 보이는데, 함수의 매개변수를 다수 받을 수 있다.
개수는 따로 정해져있지 않으며 인자의 개수만큼 연산할 수 있다.
function sumAll(...args){ //나머지 매개변수 사용
let result=0;
for(let i of args){
result+=i;
}
return result;
}
sumAll(1,2,3,4);//10
sumAll(1,2,3,4,5);//15
배열 또는 객체를 전개 구문으로 해체 한 뒤, 각 값을 변수에 할당하는 것을 의미한다.
let [x, y, ...rest] = [1, 2, 3, 4, 5];
console.log(x); //1
console.log(y); //2
console.log(rest);//[3,4,5]
function helloUser({userId: id, name: {firstName: name}}){
console.log(`"${id}" is ${name}.`);
}
let user1 = {
userId: "goodJane043",
name: {
firstName: "jane",
lastName: "star"
}
};
helloUser(user1);//'"goodJane043" is jane.'
1. Object.assign(target, source)
객체 target에 객체 source를 복사하여 하나의 객체로 합친다.
속성이 같을 경우 값은 source의 값으로 재할당되며
'얕은 복사'이다.
이에 대해서는 다음 포스트에 좀 더 자세히 설명해 놓았다.
2. 얕은 복사? 깊은 복사??
대부분의 복사 메서드로 배열이나 객체를 복사할 때,
원시 자료형은 값이 복사되지만
배열, 객체 안의 참조 자료형은 그 주소만 복사된다.
이를 '얕은 복사'라고 한다.
언뜻 복사한 배열.객체의 원시 자료형의 값을 변경했을 때
복사당한 배열.객체의 값이 변경되지 않아 복사했다고 착각하기 쉬운데,
놀랍게도 배열.객체 안의 배열.객체의 값은 변경시 원본도 같이 변경되는 걸 발견할 수 있다.
'깊은 복사', 즉 배열.객체 안의 배열.객체의 값도 복사하려면 따로 방법이 있는데 이는 나중에 추가로 배울 예정이다.
이 얕은 복사와 깊은 복사에 대해서는 다음 포스트에 좀 더 자세히 설명해 놓았다.