유데미의 클린코드 자바스크립트 강의를 듣고 정리한 내용입니다. 추후 가독성있게 수정하겠습니다.
자바스크립트 코드 스타일
- AirBnB, Google, ECMA, JShint, JSLint, Prettier, ESLint
몽키 패치 (Monkey patch) = 안티패턴
: 런타임 중인 프로그램의 내용이 변경되는 행동을 의미 (런타임 과정에서 사용자의 의도와 다르게 동작하는 것)
자바스크립트는 몽키 패치 언어다.
자바스크립트와 다른 언어들의 유사성
문법 - JAVA
문자열, 배열, 정큐, 표현식 - 펄
함수 - 오크
클로저, 스코프 환경 - 스키마
프로토타입 - 셀프
이벤트 - 하이퍼토크
JS is weird
A fun and tricky JavaScript quiz
jsisweird.com
Node.js
: 크롬 V8엔진으로 빌드된 자바스크립트 실행 환경
var 지양하기
let, const는 es2015부터 나왔다.
var은 함수 스코프, let과 const는 블록 스코프이고 Temporal Dead Zone(안전한 코드 작성 가능)이라는 속성도 가짐
var은 재할당, 재선언 가능 / let은 재할당 가능 / const는 안됨
const person={
name:'jang'
age:'30'
}
person.name='lee'
person.age='10'
-> 재할당한게 아니고 객체 내부의 값만 바꾼거라 에러 안남
const person=[
{
name: 'jo'
age: '10'
}
]
person.push({
name: 'jo'
age: '10'
})
-> 배열에 객체를 추가. 이것도 가능
=> 재할당만 못한다. 본연의 객체, 배열 같은 레퍼런스 객체를 조작할 때는 이상이 없음
전역 공간 사용 최소화
전역 공간이란?
window와 global로 나뉨. 브라우저에서 동작하는 경우 window가 최상, Node.js 환경에서는 global이 최상위
파일을 나눈다고 코드 구간도 나뉘는게 아님. 스코프가 기준
전역 공간을 더럽히면 안되는 이유
어디에서나 접근이 가능하기 때문. 사람이 생각했을 땐 구간이 분리되었다고 생각하는데 런타임에서는 분리가 안되어있음.
전역 공간으로 인해 발생할 수 있는 문제 예시
// index.js
var setTimeout='setTimeout';
// index2.js
setTimeout(()=>{
console.log('1초')
}, 1000)
-> setTimeout is not a function이라는 에러 발생. 게다가 키워드를 사용한건데 에러가 없다. 브라우저 Web API이기 때문에 js로 코드를 작성하는 단계에서 개입이 없다
더 직접적으로 알고 싶으면 IIFE (즉시 실행함수)나 모듈에 대해 알아야함
클로저 사용하는 방법도 있고 const, let을 사용해도 전역 공간을 더럽힐 확률을 낮출 수 있음.
전역 공간을 더럽히지 않는 방법
1. 전역 변수 사용X
2. 지역 변수만 사용
3. window, global에 접근하며 조작하지 않는 것
4. const, let만 사용
5. IIFE, 모듈, 클로저처럼 스코프를 나누는 방법 고민
6. 임시 변수 제거
임시 변수
: 스코프 안에서 전역변수처럼 활용되는 것
const로 선언된 임시 객체도 함수 크기가 커지면 전역공간이나 다름 없음
명령형으로 가득한 로직은 디버깅이 힘듦
타인이 추가적인 코드를 작성하고 싶어지는 유혹에 빠짐
임시 변수 제거 해결책
1. 함수 나누기
2. 바로 반환
3. 고차 함수 (map, filter, reduce)
4. 선언형 코드로 바꾸는 연습
예시1
function getElements(){
const result={};
result.title=document.querySelector('.title');
result.text=document.querySelector('.text');
result.value=document.querySelector('.value');
return result;
}
-> 쿼리셀렉터로 html에 표현된 모델, 즉 엘리먼트들을 js로 객체로 가져옴
function getElements(){
const result={
title: document.querySelector('.title');
text: document.querySelector('.text');
value: document.querySelector('.value');
};
return result;
}
-> 간단히 줄이기
function getElements(){
return {
title: document.querySelector('.title');
text: document.querySelector('.text');
value: document.querySelector('.value');
};
}
-> 더 간단히 줄이기. (CRUD 가능성을 최대로 줄이기)
예시2
fucntion getDateTime(targetDate){
let month = targetDate.getMonth();
let day = targetDate.getDate();
let hour = targetDate.Hours();
}
month = month >= 10 ? month : '0' + month;
day = day >= 10 ? day : '0' + day;
hour = hour >= 10 ? hour : '0' + hour;
return{
month, day, hour
}
}
fucntion getDateTime(targetDate){
const month = targetDate.getMonth();
const day = targetDate.getDate();
const hour = targetDate.Hours();
}
return{
month:month >= 10 ? month : '0' + month,
day:day >= 10 ? day : '0' + day,
hour:hour >= 10 ? hour : '0' + hour,
}
}
// 만약 이 함수를 수정해야할 땐 이런 방식으로
// 왜냐하면, 이 함수를 꼼꼼하게 수정하지 못하면 여러 곳에서 에러가 발생할 수 있음
function getDateTime(){
const current=getDateTime(new Date())
return{
month: current.month : '분 전',
day: current.day : '0' + '일 전',
hour:current.hour : '0' + '시간 전',
}
}
- let은 수정해서 재할당 가능하므로 const로 변경
호이스팅 주의하기
호이스팅이란?
런타임시에 선언을 최상단으로 끌어올려주는 것
선언과 할당이 분리되는 것 (런타임 시기에 예상과 다르게 동작함)
함수도 호이스팅됨 -> const를 사용해서 만든 후 함수를 할당하는 방식 추천
해결 방법
1. var 사용 X
2. 함수 조심 (함수 표현식 쓰기)
타입 다루기
- 동적인 타입은 타입검사가 어려움. 잘 찾아서 사용하기. 외우지말기..
- typeof는 타입을 문자열로 반환 (원시값 검사)모든 것에 typeof를 사용할 수는 없다. 원시값과 레퍼런스로 나뉘는데 대부분이 원시값(불변)임. 레퍼런스에는 배열, 함수, date 등이 있는데 얘네는 typeof로 감별해내기 어렵다. typeof 함수, 클래스는 'function'. 래퍼 객체는 object로 나옴 (str=new String('문자열')). null은 object로 찍히는 오류가 있음(js도 인정한 오류)
- instanceof도 많이 쓰임
객체의 프로토타입 검사.
function Person(name, age){
this.name=name;
this.name=age;
}
const p={
name: 'poco',
age: 99
}
const poco = new Person('poco', 99)
p instanceof Person // false
poco instanceof Person // true
유용해보이지만 함정이 있음. 레퍼런스 타입의 최상위는 오브젝트 타입. 프로토타입체인을 타기 때문.
const arr=[]
const func = function(){}
const date = new Date()
arr instanceof Array // true
func instanceof Function // true
date instanceof Date // true
arr instanceof Object // true
func instanceof Object // true
date instanceof Object // true
- Object.prototype.toString.call('')
래퍼 객체까지 감지 가능. 프로토타입체인을 역이용하는 것.
'[object String]' 이런식으로 출력됨
ex) Object.prototype.toString.call(new String(''))
Undefined와 null
- !를 한번 붙이면 값을 뒤집는 거고 두번 붙이면 불리언으로 형변환 시도
- null은 숫자적으로는 0임. 타입은 object
- undefined를 연산하면 NaN(Not a Number). 타입은 undefined
eqeq 출이기
동등연산자(==)를 사용하면 형변환이 발생함. 그니까 eqeqeq쓰기
isNaN
isNaN() 대신 Number.isNaN() 사용하기. js도 isNaN의 느슨한 검사를 인정하고 ES2015부터 이걸 만듦
유효하지 않은 숫자를 검사하는 기능으로, 유효한 숫자이거나 숫자타입이 아닌 경우엔 false를 반환
isNaN은 매개변수 값을 강제로 Number로 형변환함
- Number.MAX_SAFE_INTEGER // js의 최대수 출력
- Number.isInteger() // 주어진 값이 정수인지 판별
- isNaN() // 헷갈리지 말기. !== 개념임
경계 다루기
- min, max 변수 네이밍에 값 포함 여부를 표현한다.
ex) 최소값 포함 MIN_IN_NUMBER / 최소값 포함X MIN_IN_LIMIT
- begin, end는 기간 표현할 때
beginDate, endDate
- first, last는 배열 요소간 규칙이 보장 안될 때
ex) 현석, 존, 마이클 / 1,2,5,9
- prefix, suffix는 변수, 파일, 훅 등 네이밍 규칙
- Prefix 예시
- getter, setter
- 리액트의 HOOK 앞에 use
- 자바스크립트의 # private field
- Suffix 예시
- 파일 이름 끝에 s를 붙임
- 매개변수 규칙
1. 매개변수 개수는 2개를 넘지 않게
2. 2개 초과시 arguments, rest parameter 활용 or 매개변수를 객체로 만들어서 넘김(순서가 상관X)
값식문
JSX로 작성된 코드는 바벨을 만나 JS로 트랜스파일링됨 (객체로 변환)
객체 안에 값이 들어가야함. 식은 들어가면 안됨.
따라서 if문은 안되고 삼항연산자는 됨. 삼항연산자(표현식)은 값으로 귀결되기 때문.
// JSX
<div id={if(condition) {'msg}}>Hello World!</div>
// Is transformed to this JS
React.createElement("div", {id: if(condition) {'msg}}, "Hello World!");
ReactDOM.render(<div id={condition ? 'msg' : null}>Hello World!</div>, mountNode)
스위치문은 된다.
즉시 실행함수 (IIFE) 를 사용하는 예이다. 내부에 값과 식만 넣어야하는데, 즉시 실행함수는 값을 리턴하기 때문에 내부에서 스위치문을 사용할 수 있기 때문. JS의 유연성을 이용해서 트리키한 꼼수를 사용하는 방법
<p>
{(()=>{
switch(this.state.color){
case 'red':
return '#아무색상';
case 'green':
return '#아무색상2';
default:
return '#아무색상3';
}
})()}
</p>
가장 좋은 방법 (분기문 없이)
- true false 이용
{this.state.color || white}
또 다른 예시
- 좋지 않은 예
return(
<div>
{(()=>{
const rows=[];
for(let i=0; i<objectRows.length; i++){
rows.push(<ObjectRow key={i} data={objectRows[i]}/>)
}
return rows;
})()}
</div>
)
- 좋은 코드로 바꾸기 (고차함수 사용)
return(
<div>
{objectRows.map((obj, i)=>(
<ObjectRow key={i} data={obj}/>
))}
</div>
)
삼항연산자
1. 삼항연산자가 세개 이상 중첩되는 경우엔 조건을 최대한 줄이고 switch문 사용
2. 삼항연산자 식이 길어지거나 두개가 중첩되는 경우엔 괄호 사용
const example = condition1
? (a===0 ? 'zero' : 'positive')
: negative
3. 억지로 삼항연산자 쓰지말기
alert는 void를 리턴하므로 둘 다 undefined임. 그래서 딱히 의미있는 삼항연산자가 아님.
isAdult ? alert('입장이 가능합니다.') : alert('입장이 가능합니다.')
차라리 이게 낫다
if(isAdult){
alert('입장이 가능합니다.');
}else{
alert('입장이 불가능합니다.');
}
return isAdult? '입장이 가능합니다.' : '입장이 불가능합니다.'
Truthy & Falsy
불필요한 조건임
if('string'.length > 0)
if(!isNaN(10))
if(boolean==true)
그냥 이렇게 써도 됨
if('string'.length)
if(10)
if(boolean)
Falsy의 예시
- false, null, undefined, !Truthy, 0, -0, 0n, NaN, ""
활용
// 변경 전
if(name===undefined || name===null)
// 변경 후
if(!name)
Short-circuit evaluation
// 변경 전
if(state.data) return state.data;
else return 'Fetching...';
// 변경 후
return state.data || 'Fetching...';
Early Return
함수를 미리 종료하는 것. 로직엔 변경이 없으나 사람이 사고하기 편함.
변경 전
if(!isLogin){
if(checkToken()){
if(!user.nickName){
return registerUser(user);
}else{
refreshToken();
return '로그인 성공';
}
} else{
throw new Error('No Token');
}
}
}
변경 후
if(isLogin) return;
if(!checkToken()){
throw new Error('No Token');
}
if(!user.nickName){
return registerUser(user);
}
refreshToken();
return '로그인 성공';
한번 더 추상화하여 로그인 함수 만들기
function login(){
refreshToken();
return '로그인 성공';
}
if(isLogin) return;
if(!checkToken()){
throw new Error('No Token');
}
if(!user.nickName){
return registerUser(user);
}
login();
부정 조건문 지양하기
생각을 여러번 해야하는 번거로움이 있음.
그렇다면 언제 써야하나?
1. early return
2. form validation
3. 보안 혹은 검사하는 로직
예시
// 변경 전
if(!isNaN(3)) console.log('숫자입니다');
// 변경 후
function isNumber(num){
return !Number.isNaN(num) && typeof num==='number'
}
if(isNumber(3)) console.log('숫자입니다');
Default Case 고려하기
function sum(x,y){
x=x||1
y=y||2
return x+y;
}
- 엣지 케이스 생각하기
function registerDay(userInputDay){
switch(userInputDay){
case '월요일':
case '화요일':
case '수요일':
case '목요일':
case '금요일':
case '토요일':
case '일요일':
default:
throw Error('입력값이 유효하지 않음')
}
}
명시적인 연산자 사용하기
전위 연산자, 후위 연산자보다는 풀어쓰고 괄호 적극 활용하기
number-- (X)
number = number -1
Null 병합 연산자
만약 OR 연산자를 사용할 경우 0이 Falsy로 처리되어 의도와 다르게 동작할 수 있음
Null 병합 연산자는 null이나 undefined만 Falsy로 처리
function createElement(type, height, width){
const element = document.createElement(type ?? 'div')
element.style.height = String(height || 10) + 'px'
element.style.width = String(height || 10) + 'px'
return element
}
주의할 점
반드시 Null, undefined만 검사할 때만 Null 병합 연산자를 사용해야한다.
OR 연산자와 Null 병합 연산자를 혼합해서 사용하면 에러가 뜬다. 사람들이 실수를 많이한다는 이유로 문법으로 제약했음.
console.log(null || undefined ?? "foo") (X)
console.log((null || undefined) ?? "foo") (O)
드모르간의 법칙
AND 연산과 OR 연산을 이용한 연산 간의 관계로 드 모르간의 상대성이론
프로그래밍에서는 부정 연산을 다룰 때 편함
not (A or B) === (not A) and (not B)
not (A and B) === (not A) or (not B)
조건을 반대로 뒤집어야하는 경우 헷갈릴 수 있으므로 드모르간의 법칙을 적용하면 좋음
if(A && B){}
// 뒤집기
if(!(A && B){}
// 드모르간의 법칙으로 정리
if(!A || !B){}
배열 다루기
자바스크립트 배열은 객체라는 것을 명심
조건문으로 배열인지 아닌지 확인할 때
- arr.length, typeof, in Array. instanceof, t oString()... 올드한 방법.
- 특히 length는 위험함. 문자열 프로퍼티에도 length가 있기 때문. 예를 들어 notArr 객체가 length: 0이라는 값을 가지고 있다면 notArr.length===0은 true
- 따라서 Array.isArray(arr)을 사용하는게 좋음
length 사용시 주의점
자바스크립트 배열을 객체처럼 동작하기 때문에 강제로 길이를 늘리면 빈값이 들어감. 매우 큰 문제
const arr=[1,2,3];
arr.length=10;
console.log(arr.length)
// arr는 [1,2,3,,,,,,,]
const arr = [1,2,3]
arr[3]=4 // arr.length는 4
arr[9]=10 // arr.length는 10
// arr는 [1,2,3,4,,,,,,10]
따라서 배열의 length는 배열 길이보다는 배열의 마지막 인덱스에 더 가깝다
length 역이용하기
배열 초기화
1. Array.prototype.clear = function(){
this.length=0;
}
arr.clear();
2. function clearArray(array){
array.length=0;
return array;
}
clearArray(arr)
배열 요소에 접근하기
구조분해할당
예시1
operateTime([1,2],1,2) 로 전달될 때
// 방법1 (비효율적)
function operateTime(input, operators, is){
inputs[0].split('').forEach((num)=>{})
inputs[1].split('').forEach((num)=>{})
}
// 방법2 (구조분해할당)
function operateTime(input, operators, is){
const [firstInput, secondInput]=inputs
firstInput.split('').forEach((num)=>{})
secondInput.split('').forEach((num)=>{})
}
// 방법3 (인자로 받을 때부터 구조분해할당)
function operateTime([firstInput, secondInput], operators, is){
firstInput.split('').forEach((num)=>{})
secondInput.split('').forEach((num)=>{})
}
예시2
function clickGroupButton(){
const confirmButton=document.getElementsByTagName('button')[0]
const cancelButton=document.getElementsByTagName('button')[1]
const resetButton=document.getElementsByTagName('button')[2]
}
// 구조분해할당
function clickGroupButton(){
const [confirmButton, cancelButton, resetButton]=document.getElementsByTagName('button')
}
예시3
요소가 하나뿐인 배열도 구조분해할당 가능
이게 불편하다면 lodash를 이용하여 util함수 만들기. _.head(array)는 배열의 첫번째 요소를 반환함
function formatDate(targetDate){
const date = targetDate.toISOString().split('T')[0];
const [year,month, day]=date.split('-')
return `${year}년 ${month}월 ${day}일`
}
// 구조분해할당
function formatDate(targetDate){
const [date] = targetDate.toISOString().split('T');
const [year,month, day]=date.split('-')
return `${year}년 ${month}월 ${day}일`
}
// util함수 만들기
function head(arr){
return arr[0] ?? ''
}
//
function formatDate(targetDate){
const [date] = head(targetDate.toISOString().split('T'));
const [year,month, day]=date.split('-')
return `${year}년 ${month}월 ${day}일`
}
유사배열 객체 arguments
js에서 인자를 미리 명시하지 않아도 arguments를 이용하면 가변적으로 인자를 받을 수 있다. 그리고 이렇게 배열처럼 접근이 가능하다.
function printLog(){
for(let index=0; index<arguments.length; index++){
const element = arguments[index];
console.log(element);
}
}
printLog(100,200,300,400,500)
그렇다면 arguments는 배열일까? 배열처럼 고차함수(map, filter, some ...)를 사용할 수 있을까? 정답은 No.
만약 배열처럼 사용하고 싶다면 Array.from()으로 배열로 바꿔줘야함.
console.log(Array.isArray(arguments)) // false
arguments.map((arg)=>arg+'원') // arguments.map is not a function
불변성
만약 이렇게 원본 배열에만 변화를 줬을 때 newArray는 어떻게 될까?
const originArray=['123','456','789']
const newArray=originArray;
originArray.push(10);
originArray.unshift(0);
일반적으로 생각했을 때 originArray에만 변화가 있는게 정상이다. 그런데 newArray를 확인해보면 [0, '123','456','789', 10] 이다. 불변성이 지켜지지 않은 것이다.
왜 그런것일까? 같은 배열 객체를 참조하고 있기 때문이다. JS에서 배열은 객체고, 객체는 참조타입이다. 따라서 두 변수는 동일한 메모리 주소를 공유하고 있는 것이다.
불변성을 지키려면 어떻게 해야할까? 배열을 복사해서 새로운 배열을 생성하면 된다.
1. slide함수 or spread 연산자로 복사
const originArray = ['123', '456', '789'];
const newArray = [...originArray]; // 새로운 배열 복사
2. concat으로 복사
const originArray = ['123', '456', '789'];
const newArray = originArray.concat(); // 복사
3. map, filter같은 새로운 배열을 반환하는 고차함수 사용
const newArray = originArray.map(item => item); // 복사
배열 메서드 체이닝으로 리팩토링하기
1000이 넘는 원소만 필터링해서 '원'을 붙이고 정렬하는 코드이다. 보이는 것처럼 for문과 if문이 많아 깔끔해보이지 않음.
function getWonPrice(priceList, orderType){
let temp=[];
for(let i=0; i<priceList.length; i++){
if(priceList[i]>1000){
temp.push(priceList[i]+'원')
}
}
if(orderType==='ASCENDING'){
someAscendingSortFunc(temp)
}
if(orderType==='DESCENDING'){
someDescendingSortFunc(temp);
}
return temp;
}
리팩토링
더 선언적인 코드
const suffixWon=(price)=> price+'원'
const isOverOneThousand = (price) => Number(price) > 1000;
const ascendingList=(a,b)=>a-b;
function getWonPrice(priceList){
return priceList
.filter(isOverThousand)
.sort(ascendingList)
.mpa(suffixWon)
}
const result=getWonPrice(price)
console.log(result)
map VS forEach
forEach는 콜백함수를 실행시킬뿐 반환값은 없다. map은 새로운 배열을 반환한다.
따라서 반환값 유무에 따라 적절한 함수를 사용하면 된다. 이 차이를 모르고 오용하는 경우가 많다고 한다.
객체 다루기
Shorthand Properties
key-value 이름이 동일하면 축약해서 사용할 수 있다. ES2015+부터 등장한 문법이다.
const person={
firstName: fistName,
lastName: lastName,
}
// 축약
const person={
firstName,
lastName,
}
Concise Method
객체의 속성이 함수를 가지면 그걸 메서드라고 부른다. ES2015+부터 등장한 문법이다.
const person={
firstName: fistName,
lastName: lastName,
getFullName: function(){
return this.firstName + ' ' + this.lastName;
}
}
이 메서드를 보통 이렇게 축약해서 쓰는데, 이게 축약형인지 모르는 사람이 많다. JS를 배울 때 축약한 형태만 본 경우가 그렇다.
이렇게 축약한 메서드를 Concise Method라고 부른다.
getFullName(){
return this.firstName + ' ' + this.lastName;
}
Computed Property Name
대괄호를 이용해서 속성도 동적으로 다룰 수 있다.
const handleChange=(e)=>{
setState({
[e.target.name]: e.target.value,
});
};
return(
<React.Fragement>
<input value={state.id} onChange={hanldeChange} name='name'/>
</React.Fragement>
)
Lookup Table
이 switch문을 분기문 없는 코드로 리팩토링 해보자
function getUsetType(type){
switch(key){
case 'ADMIN':
return '관리자';
case 'INSTRUCTOR':
return '강사';
case 'STUDENT':
return '수강생';
default:
return '해당 없음';
}
}
Computed Property Name으로 type을 이용해서 객체에 접근할 수 있다. 현업에서 가장 많이 사용되는 방식.
function getUsetType(type){
const USER_TYPE={
ADMIN: '관리자',
INSTRUCTOR: '강사',
STUDENT: '수강생',
};
return USER_TYPE[type] ?? '해당 없음';
}
또는 이렇게도 undefined를 처리할 수 있다.
function getUsetType(type){
const USER_TYPE={
ADMIN: '관리자',
INSTRUCTOR: '강사',
STUDENT: '수강생',
UNDEFINED: '해당 없음',
};
return USER_TYPE[type] ?? USER_TYPE[UNDEFINED];
}
함수 내부에 지역 변수를 만들지 않고 객체를 바로 반환하는 팩토리 함수 형태로 더 깔끔하게 작성할 수 있다.
function getUsetType(type){
return ({
ADMIN: '관리자',
INSTRUCTOR: '강사',
STUDENT: '수강생',
}[type] ?? '해당 없음';
)
}
(getUserType("ADMIN'));
참고) JS에서는 상수를 표현할 때 스네이크 케이스를 사용함
Object Destructuring
객체 구조분해 및 할당으로 리팩토링하기
다음과 같은 코드가 있을 때 name이 필수로 들어와야한다는 것을 어떻게 표현할 수 있을까?
function Person({name, age, location}){
this.name=name;
this.age=age;
this.location=location;
}
const poco=new Person({
name: 'poco',
age: 30,
location: 'korea',
})
이렇게 하면 된다. 만약 객체분해할당이 없다면 Person 함수에서 options를 그대로 받아 this.name = options.name 이런식으로 불편하게 작성해야한다.
function Person(name, {age, location}){
this.name=name;
this.age=age;
this.location=location;
}
const options={
age: 30,
location: 'korea',
}
const poco=new Person('poco', options)
Object.freeze
Object.freeze를 사용하면 객체 속성을 수정하거나 더할 수 없다.
const STATUS = Object.freeze({
PENDING: 'PENDING',
SUCCESS: 'SUCCESS',
FAIL: 'FAIL',
});
STATUS.NEW_PROP = 'P2' // 객체 변함없음
STATUS.FAIL: 'NOTFAIL'// 객체 변함없음
객체 동결이 된게 맞는지 확인하고 싶으면 isFrozen을 사용하면 된다.
Object.isFrozen(STATUS) // true
JS를 공부했다면 얕은 복사와 깊은 복사가 무엇인지 알고 있을 것이다. freeze는 깊은 영역에는 관여를 못한다는 문제가 있다.
const STATUS = Object.freeze({
PENDING: 'PENDING',
SUCCESS: 'SUCCESS',
FAIL: 'FAIL',
OPTIONS:{
GREEN: 'GREEN',
RED: 'RED',
}
});
Object.isFrozen(STATUS.OPTIONS) // false
STATUS.OPTIONS.GREEN = 'G' // 객체 수정됨
STATUS.OPTIONS.YELLOW: 'Y'// 객체 수정됨
delete STATUS.OPTIONS.RED // 객체 삭제됨
이를 해결할 수 있는 세가지 방법이 있다.
1. loadash라는 유틸 라이브러리를 사용한다. 찾아보니 deepFreeze라는 기능이 있다.
2. 직접 유틸함수를 생성한다.
function deepFreeze(targetObj){
// 1. 객체를 순회
// 2. 값이 객체인지 확인
// 3. 객체이면 재귀
// 4. 그렇지 않으면 Object.freeze
Object.keys(targetObj).forEach(key=>{
if(/*맞다면*/) deepFreeze(targetObj[key])
})
return Object.freeze(targetObj);
}
3. 타입스크립트의 readonly 키워드를 활용한다.
이 외에도 stackoverflow에 freeze를 검색해서 다른 사람들은 어떻게 처리하나 보면 좋다.
Prototype 조작 지양하기
class가 없던 시절에는 생성자 함수를 사용해서 비슷한 동작을 하도록 만들었다.
이 과정에서 prototype을 조작하기도 했는데, 언어의 동작을 마음대로 바꿔버리는 것은 매우 강력하면서도 위험한 짓이다. 현재는 JS가 충분히 발전해서 더 이상 프로토타입을 건드릴 필요가 없다. 그러니 이러한 행위는 지양하자.
// class 사용
class Car {
constructor(name, branc){
this.name=name;
this.brand=brand;
}
sayName(){
return this.brand + '-' + this.name;
}
}
const casper = new Car('캐스퍼', '현대')
// 생성자 함수 사용
function Car(name, brand){
this.name=name;
this.brand=brand;
}
Car.prototype.sayName=function(){
return this.brand + '-' + this.name;
}
프로토타입 조작 예시
심지어 원래 있던 기능도 덮어쓸 수 있다.
String.prototype.welcome=function(){
return 'hello'
}
'str'.welcome() // 'hello' 출력
hasOwnProperty
프로퍼티를 가졌느냐를 판단하는 함수. 즉, 객체의 특정 프로퍼티 소유 여부를 반환한다.
다만 프로토타입 체인은 확인하지 않고 객체가 정의한 프로퍼티만 확인한다.
const obj={
greeting: 'hello'
}
Object.prototype.c=3;
obj.hasOwnProperty("greeting") // true
obj.hasOwnProperty("c") // false
보통 for..in과 함께 쓰인다.
for(const key in object){
if(Object.hasOwnProperty.call(object, key)){
const element = object[key];
}
}
하지만 JS는 hasOwnProperty를 보호하지 않는다. 다른 키워드에 있는 hasOwnPropety를 호출할 수 있다는 뜻이다.
prototype 내부에 있는 동작들은 보호를 받지 않는데 JS의 객체는 Object.prototype에서 확장해서 발전한다.
따라서 객체의 prototype에 접근해서 call 메서드와 함께 사용해야 안전하게 hasOwnProperty를 사용할 수 있다.
예시를 보자. 의도대로 작동했다면 true가 출력되어야하는데 같은 이름을 가진 다른 함수가 실행되어 fake가 출력됐다.
const foo={
hasOwnProperty: function (){
return 'fake';
},
bar: 'string',
}
foo.hasOwnProperty('bar') // 'fake'
이런 문제가 발생하지 않도록 다음과 같이 hasOwnProperty를 사용해야한다.
Object.prototpye.hasOwnProperty.call(foo, 'bar'); // true
재사용성을 높이고 싶다면 이렇게 함수로 따로 만들어주자.
function hasOwnProp(targetObj, targetProp){
return Object.prototype.hasOwnProperty.call(
targetObj,
targetProp,
);
}
hasOwnProp(person, 'name')
직접 접근 지양하기
객체에 직접 접근하지 말고 객체를 쓰고 지우는 함수를 따로 만들어 기능을 위임하는 게 좋다. 값의 변화를 예측 가능하고 안전하다.
객체에 직접 접근하는 방식
const model ={
isLogin : false,
isValidToken: false,
}
function login(){
model.isLogin=true
model.isvalidToken=true
}
function logout(){
model.isLogin=false
model.isvalidToken=false
}
someElement.addEventListener('click', login);
함수를 만들어 위임하는 방식
const model ={
isLogin : false,
isValidToken: false,
}
function setLogin(bool){
model.isLogin=bool
}
function setValidToken(bool){
model.isvalidToken=bool
}
function login(){
setLogin(true)
setValidToken(true)
}
function logout(){
setLogin(false)
setValidToken(false)
}
someElement.addEventListener('click', login);
Optional Chaning
. 연산자는 객체를 체이닝으로 탐색할 수 있도록 도와준다. '.' 앞에 물음표를 붙이는 것이 옵셔널 체이닝이다.
const obj={
name: 'value'
}
obj?.name
언제 사용할까?
객체나 배열에서 중첩된 속성에 접근할 때 해당 값이 null이나 undefined인 경우 오류를 방지할 수 있다.
이 response에서 useList 데이터가 유실되었다고 가정하자. 그럼 런타임에서 에러가 발생할 것이다.
const response ={
data : {
userList: [
{
name: 'Jo',
info: {
tel: '010',
email: 'jo@gmail.com'
}
}
]
}
}
console.log(respose.data.userList[userIndex].info.email)
1차적으로 이렇게 예방할 수 있다. 그런데 data도 유실되었다면? 이렇게 모든 케이스를 생각하여 코드를 작성하기는 어렵다.
if(response.data.useList){
console.log(respose.data.userList[userIndex].info.email)
}
이 때 옵셔널 체이닝을 사용한다면 간단하게 대응할 수 있다.
if(response?.data?.userList[userIndex]?.info?.email){
console.log(respose.data.userList[userIndex].info.email)
}
함수 다루기
함수, 메서드, 생성자의 this
함수의 this는 전역 객체(global)를 바라본다.
메서드의 this는 호출 객체를 바라본다.
생성자 함수의 this는 생성될 인스턴스를 바라본다.
객체의 메서드의 경우 객체 안에 함수를 넣은 것처럼 보이지만, 사실 key:value 형태인 method() : function(){}이다.(위에 써둔 Concise Method 참고)
// 함수
function func(){
return this
}
// 객체의 메서드
const obj ={
method(){
return this
},
}
// 생성자 함수
function Func(){
return this
}
복잡한 인자 안전하게 관리하기
1. 특정 매개변수가 들어오지 않았을 때 에러 처리하기
function createCar({name, brand, color, type}){
if(!name) throw new Error()
}
2. 구조 분해 할당 활용하기
function createCar ( name , { brand, color, type})
기본값 설정하기
createCarousel을 호출할 때 어떠한 인자도 넘겨주지 않는다면 어떻게 될까?
Cannot read properties of undefined 에러가 뜬다. undefined의 속성에 접근하려고 해서 그렇다.
function createCarousel(options){
var margin = options.margin || 0
var center = options.center || false
var navElement = options.navElement || 'div'
return {
margin,
center,
navElement,
};
}
createCarousel()
에러를 예방하려면 createCarousel 인자가 없어도 빈 객체를 넘겨줘야한다.
또는 함수 맨 위에 'options = options || {}'를 추가해서 기본값을 설정해준다. 마찬가지로 속성값이 falsy일 경우를 대비해서 방어 코드를 설정해줘야한다.
상당히 귀찮은 일이다. 어떻게 리팩토링할 수 있을까?
다음과 같이 구조분해할당을 할 때 기본값을 설정해줄 수 있다. 여기서 ={}은 options = options || {} 와 같다. 매개변수가 undefined인 경우 빈 객체를 할당해주는 것이다.
function createCarousel({margin =0, center=false, navElement =
'div',} = {}) {
return {
margin,
center,
navElement,
}
}
require 함수를 만들어 필수 매개변수가 들어오지 않았을 때 에러를 던지는 방법도 있다.
items가 undefined이면 required 함수가 트리거 된다.
const required = (argName) =>{
throw new Error('required is' + argName)
};
function createCarousel({
items = required('items'),
margin = 0,
center = false,
navElement = 'div',
}={}){
return {
margin,
center,
navElement,
};
}
Rest Parameters
가변 길이의 문자열을 arguments보다 더 쉽게 다룰 수 있다.
예를 들어, 일부 매개변수만 고정인 경우 고정이 아닌 매개변수는 rest parameters로 받으면 된다.
arguments와 다르게 배열이기 때문에 고차함수를 쓰기 위해 따로 변환하지 않아도 된다는 장점이 있다.
function sumTotal(
initValue, // 100
bonusValue, // 99
...args, // 1,2,3,4,5
){
return args.reduce(
(acc, curr) => acc + curr,
initValue,
);
}
sumTotal(100,99,1,2,3,4,5);
주의해야할 점은 '나머지' 이기 때문에 항상 가장 마지막에 써줘야한다는 것이다. 그렇지 않으면 동작하지 않는다.
function sumTotal(
initValue,
...args,
bonusValue,
){} // 동작 X
Void와 return
이 코드에서 고쳐야할 점은 무엇일까?
setState와 alert는 반환값이 없다. 그렇기 때문에 굳이 return을 넣을 필요가 없다.
function handleClick(){
return setState(false)
}
function showAlert(message){
return alert(message)
}
자바스크립트 함수는 return값이 없거나 return값이 undefined면 void를 반환한다.
void라는 연산자가 있으며, 함수를 만들 때 보통 function (){} 으로 만드는데 맨 앞에 void가 생략된 것이다. 따라서 void 함수명() 으로도 호출할 수 있다.
함수를 html 태그에 넣을 때 가장 많이 사용된다.
<a href = "javascript:void(document.body.style.backgroundColor='green');"></a>
<a href = "javascript:void(0);"></a>
아직 JS 기초가 부족해서 이런 형태의 코드를 처음봤기 때문에 용도를 검색해봤다.
브라우저에서 a태그의 href 값으로 javascript:를 받으면 : 뒤에 오는 코드를 자바스크립트의 코드로 해석한다고 한다.
예를 들어, <a href="javascript:alert('Hi')">Click</a> 코드를 작성하고 화면의 Click을 누르면 Hi라는 alert창이 뜨는 것이다.
a태그는 기본적으로 클릭시 새로운 페이지로 이동하거나 url을 로드한다. 만약 페이지 이동 없이 자바스크립트 코드만 실행하고 싶으면 void(0)을 써준다.
<a href="javascript:void(0)" onclick="alert('클릭되었습니다!')">클릭하세요</a>
이 코드를 보고 '굳이 이렇게까지 하면서 a 태그를 써야할 이유가 있나?' 라는 의문을 갖게 되었는데 시맨틱 의미와 접근성, UI/UX 일관성 때문에 a 태그를 써야된다고 한다.
위의 코드는 사용자에게 클릭해야하는 링크처럼 보인다. 따라서 사용자는 '클릭 가능한 요소'라는 것을 인식한다. 스크린 리더도 a 태그를 링크로 인식하고 사용자에게 알려준다. 또한, a태그는 div와 달리 키보드로 탐색이 가능하며 Tab 키를 누르면 포커스가 이동한다.
화살표 함수의 특징과 단점
무조건 화살표 함수를 사용하는 사람들이 있는데 사용시 유의점을 잘 알고 있어야한다.
1. 렉시컬 스코프를 가진다.
각 코드에서 getName의 호출 결과를 비교해보자.
위의 코드에서는 의도대로 ggubbang이 호출되는데 아래의 경우 undefined가 호출된다.
그 이유는 화살표 함수가 렉시컬 스코프를 가지기 때문이다. 그래서 화살표 함수의 this는 호출된 객체를 바라보지 않고 상위 어딘가를 바라본다.
const user = {
name: 'ggubbang',
getName(){
return this.name;
},
};
user.getName(); // ggubbang
const user = {
name: 'ggubbang',
getName: () => {
return this.name;
},
};
user.getName(); // ???
2. 함수 내부에서 arguments, call, apply, bind를 사용할 수 없다.
arguments를 사용하고 싶다면 rest parameter을 써야한다.
3. 화살표 함수로 만든 함수는 생성자로 사용할 수 없다.
const Person=(name, city)=>{
this.name=name;
this.city=city;
}
const person = new Person('ggubbang', 'korea'); // Person is not contructor
4. Class에서의 문제
- 자식 메서드에서 부모 메서드 호출 불가
화살표 함수는 생성자 함수 내부에서 바로 초기화되기 때문이다.
class Parent{
parentMethod(){
console.log("parentMethod")
}
parentMethodArrow=()=>{
console.log("parentMethodArrow")
}
}
class Child extends Parent{
childMethod(){
super.parentMethod(); // O
super.parentMethodArrow(); // 에러발생
}
}
- 오버라이딩 불가
class Parent{
overrideMethod=()=>{
return 'Parent';
}
}
class Child extends Parent{
overrideMethod(){
return 'Child';
}
}
new Child().overrideMethod(); // Parent
Callback 함수
콜백 함수는 이런 것이다. 어떤 element의 addEventListener을 호출하면 클릭 이벤트가 감지되었을 때 함수가 실행된다.
이 함수의 제어권은 addEventListener와 사용자에게 넘어간다.
someElement.addEventListener('click', function(e){
console.log(someElement + '클릭되었습니다');
})
이걸 직접 구현해보자.
리스너로 콜백함수를 받고 있고, 만약 이벤트타입이 click이라면 이벤트 객체를 만들어주고 콜백함수에 이벤트 객체를 넘겨 실행한다. params로 들어온 e는 이렇게 이벤트리스너가 대신 실행을 해주면서 이벤트 객체를 콜백함수에 넘겨준 결과이다.
DOM.prototype.addEventListener=function(eventType, cbFunc){
if(eventType==='click'){
const clickEventObject={
target:{}
};
cbFunc(clickEventObject);
}
}
이제 다음 코드를 콜백함수를 사용하여 리팩토링 해보자
function register(){
const isConfirm = confirm(
'회원가입 성공',
);
if(isConfirm){
redirectUserInfoPage();
}
}
function register(){
const isConfirm = confirm(
'로그인 성공',
);
if(isConfirm){
redirectIndexPage();
}
}
리팩토링 후
제어권을 다른 함수에 위임하였음
function confirmModal(message, cbFunc){
const isConfirm = confirm(message);
if(confirm && cbFunc){
cbFunc();
}
}
function register(){
confirmModal('회원가입 성공', redirectUserInfoPage);
}
function login(){
confirmModal('로그인 성공',redirectIndexPage);
}
순수 함수
언뜻 보면 순수 함수 같지만 비순수 함수이다. num1, num2 값을 조작할 수 있기 때문에 결과값을 예측하기 어렵다.
let num1 = 10
let num2 = 20
function impureSum1(){
return num1+num2
}
function impureSum2(newSum){
return num1+newNum;
}
순수 함수로 만들고 싶다면 다음과 같이 인자로 받아 예측 가능하도록 만든다.
function pureSum(num1, num2){
return num1+num2;
}
조금 더 어려운 예제다. 이것도 순수 함수일까?
우선 함수를 실행한 뒤 obj가 어떻게 되는지 보자. one이 100으로 변경되어 100이 출력된다.
const obj = {one:1}
function changeObj(targetObj){
targetObj.one=100;
return targetObj;
}
changeObj(obj);
console.log(obj); // 100
인자로 넘겼을 뿐인데 왜 속성이 바뀐걸까? 객체는 참조값이기 때문에 복사된 값이 아닌 메모리 위치가 전달되기 때문이다. 따라서 객체나 배열을 매개변수로 전달해야할 때는 새롭게 만들어서 반환해야한다.
const obj = {one:1}
function changeObj(targetObj){
return {...targetObj, one: 100};
}
changeObj(obj);
console.log(obj); // 1
순수 함수의 조건은 다음과 같다. 첫번째 예시는 외부 변수인 obj에 영향을 미쳤으므로 순수 함수라고 할 수 없다.
- 같은 입력에 대해서 항상 같은 출력을 반환
- 함수 외부의 상태나 변수를 변경하지 않음
- 외부 세계에 영향을 미치지 않으며, 함수 내부에서 사용되는 모든 값은 함수의 매개변수와 함수 내에서 정의된 값에 의존
클로저
클로저 활용 예시
add를 실행하면 가장 바깥의 함수(add)만 실행되고, 내부의 환경(num함수)을 기억하고 있다.
function add(num1){
return function num(num2){
return num1+num2
}
}
const addOne=add(1)
const addTwo=add(2)
addOne을 출력하면 sum함수를 품고 있는걸 알 수 있다. 그렇기 때문에 add(1)(3) 이런 식으로 활용할 수 있다.

조금 더 응용해보자. 이번에는 첫번째 컨텍스트는 첫번째 숫자, 두번째 컨텍스트는 두번째 숫자를 기억하고 세번째로 함수가 들어오면 연산 결과를 반환하는 코드다. add(1)(2)에서 1,2가 들어간 컨텍스트를 캡쳐한다.
function add(num1){
return function (num2){
return function (calculateFn){
return calculateFn(num1, num2);
}
};
}
function sum(num1, num2){
return num1+num2;
}
function multiple(num1, num2){
return num1*num2;
}
const addOne=add(1)(2);
const sumAdd=addOne(sum); // 3
const sumMultiple=addOne(multiple) // 2
또 다른 예시다.
function log(value){
return function (fn){
fn(value)
}
}
const logFoo=log('foo');
logFoo((v)=>console.log(v));
logFoo((v)=>console.info(v));
logFoo((v)=>console.error(v));
logFoo((v)=>console.warn(v));
만약 클로저를 사용하지 않았다면 if문을 여러번 써야했을 것이다.

클로저를 사용해서 이 코드를 어떻게 리팩토링할 수 있을까?
const arr=[1,2,3,'A','B','C']
const isNumber=(value)=>typeof value==='number'
const isString=(value)=>typeof value==='string'
arr.filter(isNumber)
isTypeof()가 return function 기억하고 있어 arr.filter로 arr값을 넣어주면 타입 비교값이 반환된다.
const arr=[1,2,3,'A','B','C']
function isTypeOf(type){
return function (value){
return typeof value===type
}
}
const isNumber=isTypeOf('number')
const isString=isTypeOf('string')
arr.filter(isNumber)
arr.filter(isString)
클로저로 baseUrl을 쉽게 설정할 수도 있다.
function fetcher(endpoint){
return function (url, options){
return fetch(endpoint+url, options)
.then((res)=>{
if(res.ok){
return res.json();
}
else{
throw new Error(res.error)
}
})
.catch((err)=>console.error(err));
}
}
const naverApi=fetcher('http://naver.com')
const daumApi=fetcher('http://daum.net')
getDaumApi('/webtoon').then((res)=>res)
getNaverApi('/webtoon').then((res)=>res)
추상화
적절한 숨김을 통해 더 명시적으로 표현하는 기법.
Magic Number
첫번째로 알아볼 추상화 방법은 magic number이다. 하드코딩된 숫자를 의미있는 이름으로 대체하는 것을 말한다.
이 코드를 해석해보자. 일정 시간이 지나면 스크롤을 맨 위로 올리는 코드같다. 이 때 3*60*1000 부분은 코딩을 많이 해봤다면 어떤 의미인지 짐작할 수 있지만, 어쨌든 세 번의 의식적인 흐름이 들어간다.
setTimeout(()=>{
scrollToTop();
}, 3*60*1000))
저 부분을 추상화하면 다음과 같다. 대문자와 스네이크 케이스를 사용하여 주의해야하는 값이라는 것도 알릴 수 있다.
util.ts, constant.ts 같은 파일로 옮겨 재사용할 수도 있다.
const COMMON_DELAY_MS=3*60*1000
setTimeout(()=>{
scrollToTop();
}, COMMON_DELAY_MS))
두번째로 알아볼 추상화 기법은 Numeric Operator이다. 단위를 임의로 지정해서 큰 숫자를 보기 편하게 표시하는 것이다.
const PRICE={
MIN: 1_000_000,
MAX: 100_000_000,
}
추가로, 여기서 저 MIN, MAX 값을 매개변수로 넘겨주는 함수를 호출할 때 어떻게 하는 게 좋을까?
1번보다는 매직 넘버를 사용한 2번이 더 명시적이다.
getRandomPrice(1000000, 100000000) // 1
getRandomPrice(PRICE.MIN, PRICE.MAX) // 2
비슷한 예시이다. carName 길이 범위를 어떻게 리팩토링 할 수 있을까? 당장은 불편한게 없지만 저 숫자를 사용해야할 곳이 수백개로 늘어난다고 가정해보자.
function isValidName(name){
return carName.length >= 1 && carName.length <=5;
}
이렇게 객체 형태로 추상화하면 된다.
const CAR_NAME_LEN={
MIN: 1,
MAX: 5,
}
function isValidName(name){
return carName.length >= CAR_NAME_LEN.MIN && carName.length <=CAR_NAME_LEN.MAX;
}
대문자+스네이크 케이스 조합으로 주의해야할 값이라는 것을 알릴 수 있지만 더 확실하게 수정이 불가능하게 만들 수도 있다. TS의 경우 as const를 사용하고 JS의 경우 freeze를 사용하면 된다.
// TS
const CAR_NAME_LEN={
MIN: 1,
MAX: 5,
} as const
function isValidName(name){
return carName.length >= CAR_NAME_LEN.MIN && carName.length <=CAR_NAME_LEN.MAX;
}
// JS
const CAR_NAME_LEN=Object.freeze({
MIN: 1,
MAX: 5,
})
function isValidName(name){
return carName.length >= CAR_NAME_LEN.MIN && carName.length <=CAR_NAME_LEN.MAX;
}
네이밍 컨벤션
무엇보다 JS키워드, 예약어와 겹치지 않게 짓는 것이 중요하다.
1. 대표적인 케이스
- CamelCase
- JS에서는 일반적으로 camelCase 사용
- PascalCase
- 함수, 생성자, 클래스, 컴포넌트명
- enum
- kebab-case
- NPM 패키지나 저장소명
- 파일 기반 라우팅을 하는 Next.js나 Nuxt, Remix의 파일명
- 웹의 URL
- SNAKE_CASE
- 상수 표현
2. 접두사, 접미사
- 데이터 식별자
- data-id
- data-name
- 컨테이너 이름
- AppContainer
- BoxContainer
- 컴포넌트 이름
- ListComponent
- ItemComponent
- 타입스크립트에서 Interface, Type alias 구분
- ICar
- TCar
- 특정 타입을 표현
- AType
3. 함수는 반드시 동사로 시작
일급객체 성향때문에 변수의 인자에 함수를 넘길 수 있다. 따라서 함수라는 것을 알아볼 수 있도록 네이밍 규칙을 따르는게 굉장히 중요하다.
4. 자료형 표현
JS는 동적타입 언어이기 때문에 타입을 유추할 수 있는 변수명을 짓는게 좋다.
ex) inputNumber, someArr, strToNum...
5. 이벤트 표현
- on-*
- 무언가 구독하거나 실행하는 이벤트
- handle-*
- 무언가 핸들링하는 이벤트
- 이 외에도 *-Action, *-Event, take=*, *-Query, *-All 등이 있다.
6. CRUD
- generator-* (gen-*)
- make-*
- get
- set
- remove
- creat
- delete
7. Flag (이것은 ~인가?)
- is-*
DOM API 접근 추상화
vanilla js로 구현할 때 적절히 추상화해서 내부 코드가 HTML인지 CSS인지 모르게 숨겨두는 게 좋다.
const changeColor=(element)=>{
element.style.backgroundColor="black"
}
const openModal=(element)=>{
element.classList.add('--open')
}
const closeModal=(element)=>{
element.classList.remove('--open')
}
const myModal=()=>{
// 모달 생성 코드
return document.querySelector("#modal")
}
changeColor(myModal)
openModal(myModal)
closeModal(myModal)
에러 다루기
유효성 검사란?
- 사용자의 입력 값이 유효한지 확인하는 것
- ex) 이메일의 경우 - 사용자의 입력이 이메일 포맷에 맞는지 검증한 후 맞다면 그때서야 서버와 통신
- 서버 비용을 아낄 수 있음
- 할 수 있는 모든 곳에서 다 처리하는 게 좋음. 프론트, 백엔드에서 모두 처리
유효성 검사 구현 방법
- 정규식
- regex (구현하고 싶은 기능) validation으로 검색하면 어떤 정규식을 써야하는지 나옴
- JS 문법 (문자열 검사 등)
- 웹 표준 API
- input 태그의 type, minlength 등
예외처리
try ~ catch 절을 이용하여 예외처리를 할 수 있다.
이 때 가장 많이 실수하는 게 중요하지 않다고 생각하는 핸들링을 try에 포함하지 않는 것이다. 모든 로직에 try catch를 포함하는 게 좋다.
또한 try 내부에 또 try를 넣는 것도 좋지 않은 방법이다. 함수 단위로 try를 사용해주자.
try{
// 예외가 예상되는 코드 혹은 발생시킬 코드
} catch(error){
// 예외를 처리하는 코드
} finally{
}
개발단계에서 예외처리를 네 단계로 나눌 수 있다.
- 개발자를 위한 예외처리
console.error(error)
console.warn(error)
2. 사용자를 위한 예외처리
alert('404, not found')
3. 사용자에게 사용을 제안
history.back()
history.go('어딘가로..')
clear()
element.focus()
4. 에러 로그 수집
- SETRY라는 도구로 어느 부분에서 문제가 생겼는지 자세히 알 수 있음
sentry.전송()
Brower & Web API
HTML 시멘틱요소
Node와 Element의 차이
- Node: 문서내의 모든 객체
- 아래 코드에서 노드는 html, body, main, table, thead, th이다.
- vscode의 상단을 보면 html > body > main > table > thead > th 라고 되어있는데, 여기서 '>'는 노드끼리 연결해주는 엣지이다.
- querySelectorAll을 사용하면 DOM을 한번에 가져오기 때문에 NodeList 형태로 가져온다. 따라서 사용하려면 배열로 변환해야한다. const arr=[...node]
<html>
<body>
<header></header>
<main>
<table>
<thead>
<th></th>
<th></th>
<th></th>
</thead>
</table>
</main>
- Element: Tag로 둘러싸인 요소
- ex) div 태그는 division 요소
innerHTML
innerHTML은 굉장히 오래됐고 좋지 않은 API이다.
문자열을 HTML로 해석하기 때문에 HTML을 조작하거나 JS 코드를 삽입할 수 있어 XSS 공격에 취약하다.
이를 보완할 수 있는 setHTML API가 있는데 아직 시험 단계라 사용하기엔 부적절하다.
특정 API, 포맷 등이 어느 브라우저에서 지원되는지 알고 싶다면 Can I use라는 사이트에 들어가서 이렇게 검색해보면 된다.

insertAdjustHTML
innerHTML은 선택한 DOM요소의 전체 콘텐츠를 교체하는 것이고 insertAdjustHTML은 특정 위치에 삽입하는 것이다.
innerHTML의 경우 삭제 후 재렌더링하기 때문에 성능에 부담을 줄 수 있지만 insertAdjustHTML은 기존 콘텐츠를 그대로 두고 지정된 위치에 삽입만 하므로 더 효율적이다.
사용법
첫번째 매개변수에 삽입하고자하는 위치, 두번째 매개변수에 삽입하려는 텍스트를 넣어준다.
document
.querySelector('main')
.insertAdjacentHTML('beforebegin', '<h1>Hello World</h1>')
삽입 위치는 네가지로 나뉜다.
// beforebegin
<p>
// afterbegin
foo
// beforeend
</p>
// afterend
innerText VS textContent
문자열만 렌더링하는 경우에는 innerHTML보다 innerText가 좋고, innerText보다 textContent가 더 좋다.
- innerText
- 숨겨진 콘텐츠를 제외하고(ex) display: none ) 화면에 있는 텍스트를 그대로 읽어온다.
- textContent
- 모든 텍스트 노드를 읽어온다. 그렇기 때문에 innerText와 다르게 리플로우가 발생하지 않는다. 리플로우 계산은 비싸기 때문에 피하는 게 좋다.
- XSS 공격에 방어적이다. innerText는 브라우저 렌더링 엔진에 의존해 특정 환경에서 의도치 않게 HTML로 해석될 수 있는 방면에, textContent는 HTML 태그를 단순 텍스트로 변환한다.
리플로우란?
브라우저의 렌더링 과정을 살펴보면 마지막에 렌더트리를 기반으로 레이아웃과 페인팅을 한다. 이 때 DOM이나 CSSOM의 구조가 변경되어 이 작업을 다시 실행하는 것을 리플로우라고 한다.
insertAdjacentText
문자열 역시 특정 위치에 삽입할 수 있다. 사용 방식은 insertAdjustHTML과 같다.
데이터 속성
비표준적인 요소를 더 의미있게 사용할 수 있도록 제한해주는 것이다.
HTML에서 다음처럼 비표준적인 요소를 넣어도 에러가 나지 않는다.
<h1 안녕="하세요">Hello World</h1>
<main hello="world">main</main>
케밥 케이스로 조금 더 안전하게 표준을 지키며 데이터 속성을 커스텀해서 사용할 수 있다.
<main
id="electriccars"
data-colums="3"
data-index-number="123"
data-parent-world="cars"
>
</main>
JS에서 getAttribute로 이렇게 만든 속성에 접근이 가능한데 이것보다 직접 접근하는 것을 더 추천한다.
data-* 접두사는 지워지고 dataset 객체를 통해 모든 data-* 속성을 읽을 수 있다.
var main=document.querySelector('main')
main.dataset.columns;
main.dataset.indexNumber;
main.dataset.parentWorld;
하지만 객체에서는 케밥케이스를 사용할 수 없다는 문제가 있다. 카멜케이스를 사용해도 인식이 되기 때문에 케밥케이스 대신 카멜케이스를 사용하면 된다.
const obj={
indexNumber:
}
delete로 쉽게 삭제도 가능하다.
delete main.dataset.columns;
css에서도 접근이 가능하다.
article::before{
content:attr(data-parent)
}
article[data-columns='3']{
width: 400px
}
cypress같은 도구를 사용할 때 이런 데이터 속성 형식을 많이 쓰는데, 만약 개발단계에서만 필요한 데이터 속성이라면 빌드 단계에서 웹팩이나 vite를 이용하여 지우는 것을 추천한다.
'나도 공부한다 > JS' 카테고리의 다른 글
디바운싱과 쓰로틀링 (0) | 2024.02.20 |
---|---|
var을 사용하면 안 되는 이유 (0) | 2022.11.14 |