프론트엔드 데브코스 첫 날을 진행하면서 강의를 듣던 도중 강사님께서 앞으로 어떻게 공부하면 좋을지에 대해 말씀해주셨다.
1. 기본기를 탄탄하게 하고, 상식을 기를 것 (처음 공부할때 제대로 익히는게 중요!)
2. 정답을 찾는 과정을 즐길 것 (내가 부족한 점.. 충분한 고민을 해볼 것!)
3. 내가 무엇을 잘하고 못하는지 파악할 것 (내가 모르는 것과 아는 것을 정확하게 알아야 함 -> 데브코스에 지원하게 된 동기이기도 하다)
4. 공부한 것은 꼭 기록하기
5. 질문을 두려워하지 않기 (고민하는 시간을 가지는 것이 무척 중요하지만 너무 길이어지지는 않도록 하기)
이중에서 4,5번이 특히 찔렸는데 이전에는 github에 TIL 레포를 만들어서 기록했었지만 막상 다시 기억이 안나서 보려하면 레포를 하나하나 뒤지면서 찾기가 번거로워서 그냥 구글링을 통해 다시 찾아봤었던 것 같다...
다른 분들은 어떻게 기록을 하나 살펴보니 다들 블로그 (벨로그가 특히 많았던 것 같다.) 를 운영하시는 것 같아서 찾아봤더니 깃헙 레포를 통해서 작성할때보다 더 영역별로 분리해서 보기도 좋고 ui도 더 깔끔한 것 같아서 나도 벨로그를 통해 그 날 배운 내용을 정리하고 정리하면서 모르는 부분은 질문하기로 마음먹게 되었다.
첫 날은 앞으로 주구장창 사용하게 될 JS의 기초에 대해 배웠다. 아는 내용도 있고 정확히 몰랐던 내용도 꽤 많아서 이미 인지하고 내용은 간략하게, 정리가 더 필요한 내용 위주로 기록을 하기로 했다.
1. 브라우저 동작 원리
2. 프론트엔드 개발자의 역할
3. 변수 및 상수
5. 자료형
6. 메모리
7. 표현식과 연산자
8. 흐름 제어
9. 배열 및 객체
10. 스코프와 클로저
브라우저의 동작은 크게 다음 3가지로 나눌 수 있다.
- 통신
- 렌더링
- 스크립트 실행
말 그대로 서버와의 통신을 말함. 브라우저가 서버로 요청을 보내면 서버는 요청에 따른 응답값을 보내줌 (요청은 한번에 하나씩 할 수도 있고 동시에 할 수도 있음)
렌더링이란 돔이라 부르는 객체를 화면에 그리는 것을 말함. (돔이란 통신을 통해 받은 html을 브라우저가 읽어서 생성된 것, 트리 구조가 특징)
말 그대로 JS를 실행시키는 것을 의미. 브라우저가 script탭을 통해 JS 파일을 읽으면 실행. (이를 통해서 UI에 이벤트를 등록하는 등 동적인 화면 구성 가능)
가장 큰 역할은 '브라우저에서 동작하는 UI를 개발하는 것'.
프론트엔드 개발자는 개발 능력뿐만 아니라 다른 직군과 소통하는 능력 역시 매우 중요하다! (다방면의 공부가 필요)
- 커뮤니케이션
- UI
- 네트워크
- 보안
- 브라우저
- 디자인
- 컴퓨터 과학 무시
알고리즘, 설계 패턴, 메모리...- css 안하기
퍼블리셔가 하는 일이라 단정 짓지 않기!- 코더가 되는 것
프레임워크, 라이브러리에 기능을 가져다 쓰지만 하면 안됨 -> 항상 도전하고 공부하는 자세 가질 것
컴퓨터 공학에서 변수란, '메모리에 할당한 값' 을 말함. 변수를 선언하기 위한 키워드로는 var, let이 있음 (var의 경우 ES6 이전의 문법으로 권장하지 않음 -> let을 사용하자)
그렇다면 왜 var를 사용해야 할까?
이유는 '호이스팅' 때문
MDN 문서의 설명에 따르면
JavaScript에서 호이스팅(hoisting)이란, 인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미합니다.
var로 선언한 변수의 경우 호이스팅 시undefined로 변수를 초기화합니다. 반면let과const로 선언한 변수의 경우 호이스팅 시 변수를 초기화하지 않습니다.
좀 더 호이스팅에 대해 알고 싶다면 다음 MDN 문서를 살펴 보자.
변수 선언 예시
let variable = 1
위의 예시에서 let은 키워드, variable은 메모리 상 주소, 1은 주소에 해당하는 값을 의미한다.
예를 들어, '서울시 마포구 마포대로 122'는 대한민국에서 사용되는 주소명, 그에 속하는 위도 및 경도는 실제 지구상의 위치를 의미한다.
이것을 컴퓨터에 대입하면 '서울시 마포구 마포대로 122'는 변수명에 해당하고 실제 위도 및 경도(GPS 좌표)는 메모리 주소, 해당 위치에 지어진 건물은 메모리에 주어진 상태 즉, 값이라 볼 수 있다. (집을 부수고 새 건물을 짓는 것처럼 메모리상 값도 언제든지 변경이 가능)
변하지 않는 수를 의미. 위의 예시를 통해 이어서 설명하면 '서울시 마포구 마포대로 122'에 지어진 건물을 법적으로 건물을 허물 수 없도록 막은 것과 같다. (즉, 변경이 허용되지 않음)
상수 선언 예시
const variable = 1
JS의 기본 자료형은 다음 8가지가 있다.
- Number
399, 39.9, NaN, Infinity...- String
'문자열', "문자열"...)- Boolean
true, false- Object
여러 값을 지닐 수 있는 타입이라 생각, 'key'(문자열) 와 'value'를 통해 구성- Array
여러 값을 가질 수 있음, 'key'가 아닌 'index'를 통해 값을 탐색- Function
- Undefined
변수 혹은 상수가 선언되었지만 아무런 값도 대입되지 않은 경우- Null
변수 혹은 상수가 선언되고 null값을 넣음, 의도적으로 비어있음을 나타낼 때 사용
일반적으로 메모리는 다음 3가지 과정을 거침.
- 할당
변수를 생성하면 메모리 한 구역에 할당됨- 사용
할당된 메모리에 값을 넣어 사용 가능- 해제
사용을 마치면 해제해서 메모리 제거
메모리는 한정되어 있으며 변수, 상수를 만들때마다 공간이 줄어듬. (메모리가 꽉차면 프로그램이 터지기 때문에 메모리 해제가 필요!)
JS에서는 'Garbage Collector'를 통해 메모리를 종료함. 'Garbage Collection'이라는 자동 메모리 관리 알고리즘을 통해 만들어진 객체로 사용하지 않는 메모리를 해제하는 역할. (우리가 메모리에 대해 크게 신경쓰지 않아도 되는 이유 -> 최적화를 위해 신경써야할 수 있지만 직접 관리해야하는 C++같은 언어에 비해 편리한 편)
그렇다면 변수를 선언할때 JS 내부에서는 어떤 일이 발생할까?
변수의 고유식별자를 생성하고 메모리의 주소를 할당. 그리고 생성한 주소에 값을 넣는다.
let variable = 1
위의 코드에서 variable은 값인 1을 바라보고 있는 것이 아니라 값인 1이 들어가있는 메모리 주소를 바라보고 있게 된다. 그렇다면 아래처럼 새 변수에 위의 variable을 대입하면 어떻게 될까?
let variable = 1
let variable2 = variable
variable2는 variable의 메모리 주소를 참조하게 된다. (해당 주소의 값에는 1이 담겨있으므로 variable2의 값도 1이 된다.)
그렇다면 아래 코드처럼 variable 값을 변경하면 어떻게 될까?
let variable = 1
let variable2 = variable
variable = variable + 1
variable이 새로운 메모리 주소를 할당받게 되고 그곳에 값을 넣게 된다. (JS의 원시타입은 변경이 불가하기 때문 -> 원시타입의 값이 변경될때는 항상 메모리가 새로 할당됨.)
JS엔진은 가상머신(Virtual Machine)으로 구성되어 있는데 이곳에는 메모리 모델인 Heap 영역과 Call Stack 영역이 있다. (Heap에는 참조 타입, Call Stack에는 원시 타입이 들어감)
다음의 코드 예시를 보자.
let 변수1 = 1; // call stack에 쌓임
const 상수1 = 2; // call stack에 쌓임
const 배열1 = []; // 배열은 object type이기 때문에 참조타입 -> heap에 배열 영역이 생성되고 call stack에 생성된 '배열 변수'는 heap에서 생성된 배열의 메모리 주소를 참조하게 됨
// heap 영역 메모리는 동적으로 크기가 변할 수 있음 -> 배열에 값을 추가하면 heap 메모리 영역에 그대로 할당 됨 (배열을 상수로 선언해도 동작하는 이유: call stack에 할당된 메모리를 변경하는 것이 아니라 heap 메모리를 변경하는 것이기 때문에)
배열1.push(3);
배열1.push(4);
배열1.push(5);
그렇다면 사용을 마친 메모리는 어떻게 정리될까?
위에서 언급했던 'Garbage Collector'에 의해 정리되는데, 'Mark and Sweep Algorithm' (닿을 수 없는 주소를 더 이상 필요없는 주소로 정의하고 지우는 알고리즘) 을 통해 메모리를 정리하게 된다.
'Mark and Sweep Algorithm'은 브라우저 최상위 객체인 'window'에서 시작해 닿을 수 없는 곳은 필요없는 주소로 여기고 이를 지우게 된다.
let variable = 1
let variable2 = variable
variable = variable + 1
variable2 = variable + 1
위의 코드에서 메모리 값은 1, 2, 3으로 3개이고 variable은 2, variable2는 3을 참조하고 있다. 첫 번째 메모리 값인 1은 더 이상 아무도 참조하고 있지 않으므로 이 값은 'Garbage Collector'에 의해 사라지게 된다.
메모리가 지워지는 것은 참조가 중요하며 밑에서 언급할 '클로저'가 가능한 이유도 참조 덕분이라고 할 수 있다.
일반적으로 웹사이트는 여러개의 JS로 이루어져 있다. (각 파일들을 별개의 프로그램으로 취급)
그렇다면 JS 프로그램은 무엇으로 이루어져 있을까?
답은, 표현식(Expressions) 와 문장(Statements)
표현식이란, 어떠한 결과 값으로 평가되는 식을 말함. (결과가 계산되는 식: 숫자, 문자열, 논리값과 같은 원시 값을 포함하여 변수, 상수, 함수 호출 등으로 조합 가능)
표현식 예시
const num = 1+2;
const hi = num-1;
const hello = 'str' + 1 // 'str1'
const bye = true + true + false // 2
오른쪽 표현식을 왼쪽 피연산자 값에 할당하는 연산자를 말함. ( 등호(=)를 사용, 다른 연산자와 같이 사용해 복합 할당 연산자로 이용 가능 )
// 할당 연산자
let a = 5;
// 복합 할당 연산자
a += 1;
a -= 1;
a *= 1;
a /= 1;
a %= 1;
a <<= 1;
a >>= 1;
좌측 피연산자와 우측 피연산자를 비교하는 연산자 (true 혹은 false 반환)
const a = 1;
const b = 2;
a == b; // false (동등)
1 == '1' // true
a != b; // true (부등)
a === b; // false (일치)
1 === '1' // false
a !== b; // true (불일치)
a > b; // false
a >= b; // false
a < b; // true
a <= b; // true
덧셈, 뺄셈, 곱셈, 나눗셈을 하는 연산자를 말함. (Number 반환)
+-*/: 사칙 연산 사용
비트를 직접 조작하는 연산자 (자주 쓰이진 않지만 코테 등에 사용됨)
const x = 10; // 1010
const y = 12; // 1100
x & y; // 8 (AND)
x | y; // 14 (OR)
x ^ y; // 6 (XOR)
~x; // 11 (NOT)
x << 1; // 20 (10100)
x >> 1; // 5 (101)
Boolean을 통해 참과 거짓 검증
const a = true;
const b = false;
a && b; // false
a || b; // true
!a; // false
조건에 따라 값을 선택하는 연산자 조건 ? 참 : 거짓 형태
const a = 1;
const b = 2;
a > b ? 5 : 10 // 10
객체에 속성이 있는지 확인하기 위한 연산자
const obj = {
name: 'Kim Jeong Ho',
email: 'cloud0406@naver.com',
};
'name' in obj; // true
'gender' in obj; // false
피연산자의 타입을 반환하는 연산자 (문자열 리턴)
const num = 1;
const str = '김정호'
const boo = true;
typeof num // number
typeof str // string
typeof boo // boolean
흐름을 제어하는 방법 중 하나로, 조건이나 반복을 통해 상태를 제어하는 것을 말함.
'Control Flow'와 'Data Flow' 2가지 방식으로 제어 가능.
조건분, 반복문 등이 포함 (if, then, else, switch, case, for, while)
for (let i=0; i<3; i++) {
if(i === 1) {
console.log(i);
}
}
함수형 프로그래밍 방식으로 구현 가능 (stateless, recursion, pipe)
[1,2,3]
.filter((i) => i % 3 === 0)
.forEach((i) => console.log(i));
조건이 맞을 때만 실행되는 문장 문법을 말함 (Control Flow)
오늘 기분이 꿀꿀한가?
-> Yes -> 운동 안함
-> No -> 운동 감
if문을 통해 구현 가능 (else if, else 같이 사용 가능)
주의할 사항은 if 안쪽 조건이 false가 아니더라도 undefined, null, 0, NaN, ''(비어있는 문자열) 이라면 거짓으로 적용되므로 주의할 것!
switch를 통해서도 구현 가능 (괄호안 값에 따라 분기 -> case, default와 함께 사용)
const a = 1;
swtich (a) {
case 1:
console.log('1');
break; // break를 사용하지 않을 경우 다음 case도 실행되게 됨
case 2:
console.log('2');
break;
case 3:
console.log('3');
break;
default:
console.log('default');
}
반복적인 작업 지시
공부하자 -> 반복{오늘 기분이 꿀꿀한가? ->(no) -> 공부하자 -> 오늘 기분이 꿀꿀한가? ->(no) -> 공부하자 -> 오늘 기분이 꿀꿀한가? ->(no)} -> 오늘 기분이 꿀꿀한가? ->(yes) -> 안 한다.
초기문, 조건문, 증감문으로 이루어짐 (조건문 결과가 거짓이면 반복 종료)
for (초기문; 조건문; 증감문)
괄호안 조건이 거짓 될 때까지 반복
while문과 다르게 먼저 진입해서 로직 실행 후 다음 조건 검사
let a = 1;
do {
console.log('hi);
} while(a>5);
연관된 데이터를 연속적인 형태로 저장하는 복합 타입 (순서대로 index가 붙음)
배열 생성 예시
const arr = [];
const arr2 = [1,2,3];
const arr3 = new Array();
const arr4 = new Array(5);
// 특정한 값으로 배열 초기화
const arr5 = new Array(3).fill(1); // [1,1,1]
// function의 첫 번째 파라미터는 배열의 값, 두 번째 파라미터는 배열의 인덱스
const arr6 = Array.from(Array(3), function (v,i) { // [1,2,3]
return i+1;
});
// 배열의 길이 확인
console.log(arr.length);
// 길이를 조작할 수도 있음 -> 설정한 길이 이후의 요소들은 제거됨 or 길게 설정할 경우에는 빈값으로 채움 : 이러한 방법은 권장 x)
arr4.length = 3;
const arr = [1,2,3];
const arr2 = [4,5,6];
// join: 배열을 괄호안에 요소를 통해 합침
console.log(arr.join(', ')); // 1, 2, 3
// reverse: 배열 뒤집기 -> 원래 배열 자체를 변경시키므로 주의해서 사용
console.log(arr.reverse()); // [3,2,1]
console.log(arr); // [3,2,1]
// concat: 배열 합치기
console.log(arr.concat(arr2)); // [3,2,1,4,5,6]
arr = [1,2,3];
// slice(start, end): start 인덱스부터 end-1 인덱스까지 요소를 자름 (원본 배열 변화 x)
console.log(arr.slice(1,3)); // [2,3]
console.log(arr); // [1,2,3]
// splice(start, count): start 인덱스부터 count 만큼 요소 삭제 (원본 배열 변화함)
console.log(arr.splice(1,1)); // [1,3]
console.log(arr); // [1,3]
arr = [1,2,3];
// for of
for (const item of arr) { // item: 1, 2, 3
console.log(item);
}
// 배열은 객체와 타입이 동일
console.log(typeof arr); // object
arr["key"] = "hi";
console.log(arr); // [1,2,3,key: 'hi']
console.log(arr.length) // 3 (key를 추가 했음에도 배열 길이는 그대로 -> 배열의 길이는 내부적으로 관리되기 때문에 객체처럼 사용해도 길이에는 변화가 없다. -> 추천하지 않는 사용 방법)
외에도 배열의 뒤에 요소를 추가하는 push, 뒤의 요소를 삭제하는 pop
앞에 요소를 추가하는 unshift, 앞의 요소를 삭제하는 shift가 있다.
여러 값을 key-value 형태로 결합시킨 복합 타입
객체 생성 예시
const obj = new Object();
const obj2 = {};
const obj3 = { name: '김정호', job: '백수'};
// 추가
obj3['email'] = 'cloud0406@naver.com';
obj3.hi = 'hello';
console.log(obj3); // { name: '김정호', job: '백수' , email: 'cloud0406@naver.com', hi: 'hello'};
// 삭제
delete obj3.hi;
console.log(obj3); // { name: '김정호', job: '백수' , email: 'cloud0406@naver.com'};
// key값 확인
console.log('name' in obj3); // true
console.log('hi' in obj3); // false
// key, value 집합 구하기
console.log(Object.keys(obj3)); // ['name', 'job', 'email']
console.log(Object.values(obj3)); // ['김정호', '백수', 'cloud0406@naver.com']
// for in (객체 순회)
for (const key in obj3) {
console.log(key, obj3[key]); // name 김정호 / job 백수 / email cloud0406@naver.com
}
유효 범위라고도 부르며 변수가 어느 범위까지 참조되는 지를 뜻함.
Global Scope: 어디서든 접근 가능, Local Scope: 해당 컨텍스트 내에서만 접근 가능
const global = 1; // global scope
{
const local = 2; // local scope
console.log(global, local); // 1 2
}
console.log(a,b); // Error 발생
var 사용시 호이스팅으로 인해 예상히 못한 오류 발생 가능성 있음 (let, const을 사용하자!)
var 함수 수준 스코프, const, let은 블록 수준 스코프
var a = 1; // global scope
{
// 호이스팅으로 인해 변수 선언이 상단으로 올라감
var a = 2; // local scope
console.log(a); // 2
}
console.log(a); // 2 (블록 내부에서 재선언해도 외부에 영향을 끼침)
함수가 선언된 환경의 스코프를 기억하여 함수가 스코프 밖에서 실행될 때에도 기억한 스코프에 접근할 수 있게 만드는 문법
function makeGreeting(name) {
const greeting = 'hi, '; // 지역 스코프라 함수 종료시 메모리에서 사라짐
return function () {
console.log(greeting + name);
};
}
const jeongho = makeGreeting('김정호');
jeongho(); // 실행 시점에 greeting이 살아있음 (접근 가능)
클로저를 이용하여 내부 변수와 함수를 숨길 수 있음.
function Counter() {
let localCount = 0;
fucntion changeCount(v) {
localCount += v;
}
return {
increment: function() {
changeCount(1);
},
decrement: function() {
changeCount(-1);
},
value: function() {
return localCount;
};
};
}
const counter = Counter();
console.log(counter.value()); // 0
counter.increment();
counter.increment();
console.log(counter.value()); // 2
counter.decrement();
console.log(counter.value()); // 1
클로저를 잘 알아야하는 이유는 유용하게 사용하기 위함 보단 버그를 잘 수정하기 위해서이다!
다음 코드의 결과가 어떻게 될지 생각해보자
function counting() {
let i=0;
for (i = 0; i<5; i++) {
setTimeout(function () {
console.log(i);
}, i*100);
}
}
counting();
결과는
5 5 5 5 5//1 2 3 4 5를 생각했다면 클로저에 대한 개념이 부족한 것(나야 나)
setTimeout의 대기시간이 끝나 콜백함수가 실행되는 시점에는 반복이 종료되기 때문에 i가 5가 되어있기 때문에 위의 결과가 나오게 된다.
이를 해결하기 위한 방법으로는 2가지가 있는데
즉시 실행 함수를 통해 루프마다 클로저를 만드는 방법
function counting() {
let i=0;
for (i = 0; i<5; i++) {
(function (number) {
setTimeout(function () {
console.log(number);
}, nuber*100);
})(i);
}
}
counting();
let은 블록 수준 스코프이기 때문에 매 루프마다 클로저가 생성됨.
function counting() {
for (let i = 0; i<5; i++) {
setTimeout(function () {
console.log(i);
}, i*100);
}
}
counting();