클린코드 자바스크립트
Goal
Udemy 클린코드 자바스크립트를 듣고 학습한 내용 정리하기
변수 다루기
var를 지양하자
var
는 함수 스코프let
&const
는 블록 레벨 스코프 + TDZconst
는 재할당 불가능,let
은 할당 가능
function scope & block scope
var
는 함수 스코프이기 때문에 블록 스코프(if문 등...) 내에서 변경되면 전역 공간에서까지 변경이 된다.let
과const
는 안전하게 블록 스코프이다.
객체에서 "재할당" 의 의미
const person = {name: 'jang',age: '20'}person.name = 'choi' // 가능, 재할당이 아닌 값만 바뀐 것이라서
배열도 마찬가지로 재할당이 아닌 값을 바꾸는 것은 가능하다.
전역 공간 사용 최소화
왜 전역 공간을 사용하면 안될까?
- 경험에 의해 알 수 있음
- 누군가 혹은 js 생태계에서 지속적으로 사용하지 말라고 추천하기 때문에
- 강의 혹은 책에서 권함
- 회사 혹은 멘토가 권함
- lint 에서 알려주는 경우
전역 공간이란 ? 최상위 공간
- 브라우저 환경에서
window
가 최상위 - node.js 환경에서
global
이 최상위 - 전역 공간에서 변수를 사용하면 파일을 나눠도 코드 구역이 나눠지지 않고 모든 파일에서 사용이 가능하다.
- 따라서 어떤 파일에서 사용하는 함수를 다른 파일에서 같은 이름으로 덮어씌워 버리면 ? -> 오류가 생길 수 있다.
var
를 사용하지 않으면 된다.
전역 공간 사용을 최소화하는 방법
임시변수 제거하기
임시 변수란?
어떤 scope 안에서 전역 변수로 사용되는 것
- 임시 객체도 함수가 커지면 전역 공간이나 다름없는 상황이 된다.
- 잘게 함수를 쪼갠다면 문제 없지만 함수가 커졌을 때 임시변수를 만드는 습관이 있다면 같이 일하는 팀원이나 자신이 나중에 유혹을 받을 수 있음...
그렇다면 임시변수, 객체는 어떻게 CRUD 할까?
- 함수에서 바로 반환하기. 임시변수 없이
함수가 할 수 없는 추가적인 스펙(기능이나 마케팅) 이 생긴다면?
- 함수를 수정하기 -> 이 함수를 수정했을 때 꼼꼼히 확인하지 못하면 사용하는 다른 코드에서 많은 영향을 끼침
- 함수를 추가로 만들기 -> 함수를 한번 더 추상화하기
임시 변수를 제거해야 하는 이유
- 명령형으로 가득한 로직
- 어디서 어떻게? 디버깅이 힘들다.
- 추가적인 코드를 작성하고 싶은 유혹이 들게 만든다. (함수는 하나의 역할만 해야 하는데... ) 코드의 유지보수가 어렵다.
명령형에 가까운 코드는
- 임시 변수가 함수 내부 여러 부분에서 바뀌고 연산이 되면서 반환이 된다.
- 임시변수의 값을 추정하기가 어렵다. -> 임시 변수 사용하지 않기가 중요하다!
임시 변수 제거의 해결책
- 함수가 하나의 역할만 하도록 나누기
- 바로 반환하기
- 고차함수 사용하기
map
,filter
,reduce
- 선언형 코드 작성하기
호이스팅 주의하기
호이스팅?
- 호이스팅은 런타임 시기에 선언과 할당이 분리된 것
- 스코프의 동작을 예상하며 코드를 작성하는데 런타임에선 예상대로 동작하지 않는 경우가 있다. 그 현상 중 하나가 바로 호이스팅. 따라서 호이스팅이 일어나면 예측하지 못한 오류가 발생할 수 있다.
호이스팅으로 인한 실수를 줄이는 방법
var
사용하지 않고let
,const
사용하기- 함수도 호이스팅되기 때문에 함수 표현식 사용하기,
const
로 선언하기
타입 검사
typeof
연산자는 피연산자를 평가해서 문자열로 반환해준다.
typeof '문자열' // 'string'typeof true // 'boolean'typeof undefined // 'undefined'typeof 123 // 'number'typeof Symbol() // 'symbol'
원시 타입과 객체 타입
PRIMITIVE vs REFERENCE reference는 [[JavaScript-Data-Type#typeof|typeof 연산자]]로 확인하기 어렵다.
function myFunction() {}class MyClass {}typeof myFunction // 'function'typeof MyClass // 'function'const str = new String('문자열')typeof str // 'object'typeof null // 'object'
그러므로 typeof
가 항상 내가 원하는 대로 결과를 반환해주지 않는다. (typeof null
은 프로그래밍 언어 설계상 오류로 인정했음)
자바스크립트는 동적으로 변하는 언어이기 때문에 타입도 동적이다. -> 타입을 다룰 때 주의가 필요하다.
instanceof
function Person(name, age) {this.name = name;this.age = age;}const poco = new Person('poco', 99)const p = {name: 'poco',age: 99}poco instanceof Person // truep instanceof Person // false
const arr = []const func = function() {}const date = new Date()arr instanceof Array // truefunc instanceof Function // truedate instanceof Date // true// 프로토타입 체인을 타기 때문에 최상위에 있는 Object의 instance 도 된다.arr instanceof Object // truefunc instanceof Object // truedate instanceof Object // true
헷갈릴 수 있기 때문에 타입을 검사할 때 주의가 필요한 것
팁
- 타입 확인에 대해 구글링할 때
- "javascript is function"
- "javascript is array"
- "javacript is string" 등으로 검색하기
- stackoverflow에서 추천을 많이 받은 답변도 중요하지만 그 의견이 언제 올라온 것인지 확인하는 것도 중요!
요약
- 자바스크립트라는 언어는 동적인 타입을 가지는 언어이다. -> 타입 검사가 어렵다...
- 타입 검사 시 확인 방법에 대해 다 외우는 것은 힘들기 때문에 잘 찾아내기(검색 활용)
- Primitive vs Reference 의 차이도 잘 확인
typeof
가 무적이 아니다.instanceof
사용하는 방법도 있음
undefined & null
!null // true!!null // falsenull === false // false!null === true // true// null -> 숫자 연산시 0으로 취급null + 123 // 123
let varb; // 선언했지만 값은 정의되지 않고 할당 xtypeof varb // 'undefined'undefined + 10 // NaN!undefined // trueundefined == null // trueundefined === null // false!undefined === !null // true
null
값과 undefined
를 사용할 때 매우 혼란스럽다... 팀에서 개발을 할 때 우리는 비어 있는 값은 null
로 하자, undefined
로 하자 등.. 컨벤션이 있는 것이 좋다. 개인적으로도!
undefined
, null
비교
null
은 값이 없다고 명시적으로 표현하는 것undefined
-> 숫자와 연산하면NaN
-> type은undefined
null
-> 숫자와 연산하면0
-> type은object
undefined
와null
은 사용할 때 항상 조심해야 한다.
eqeq 줄이기
- eqeq: 동등 연산자
==
를 완벽하게 생각하면 안 된다!===
strict equality 완벽하게 동등한지 확인이 가능. 엄격한 동등 연산자==
와===
는 큰 차이를 불러온다.==
을 사용하면 형변환(type casting)이 일어난다. 위험하다.
// =='1' == 1 // true1 == true // true// === 사용하기'1' === 1 // false1 === true // false
형변환은 꼭 수동으로해서 비교하자!
// ticketNum은 input elementticketNum.value == 0 // xNumber(ticketNum.value) === 0ticketNum.valueAsNumber === 0
형변환 주의하기
// 암묵적 변환11 + ' 문자와 결합' // '11 문자와 결합'!!'문자열' // true!!'' // falseparseInt('9.9999', 10) // 9// 두번째 파라미터를 지정하지 않으면 10진수가 기본값은 아니다.// 그래서 10진수를 원한다면 꼭 지정하는 게 중요// 명시적 변환String(11 + '문자와 결합')Boolean('문자열')Number('11')
- 명시적 변환: 사용자가 형변환을 하는 것
- 암묵적 변환: js가 평가하는 것
- 타입을 변환할 때 명시적으로 해줄 필요가 있다. -> 예측하기 쉬운 코드
isNaN
- 사람이 생각하는 숫자는 10진수인데, 컴퓨터는 2진수를 사용한다.
- 이 차이 때문에 코드를 작성할 때 실수를 많이 한다.
- 소수점을 사용할 때 제일 문제
- 이 간극을 채우기 위해 IEEE 754 표준을 통해 해결하려고 한다.
- 부동소수점 -> 떠돌이 소수점
- 사람과 컴퓨터 생각의 차이의 예로
isNaN
은 결과가 거꾸로 나온다.- 이 말이 무슨 말이냐하면, 사람의 사고로는 보통
true
가 나오도록 코드 작성을 한다.typeof 123 === 'number'
가true
와 같은 식으로 하지만isNaN(123)
은false
로 나온다.
- 이 말이 무슨 말이냐하면, 사람의 사고로는 보통
isNaN(123)
, 전달한 인자가 숫자가 아니다. => 숫자가 맞다. 한 번 더 생각해야 함isNaN(123 + '테스트')
=>true
Number.isNaN(123 + '테스트')
=>false
- 숫자가 아니냐 맞냐를 할 때는
Number
붙이도록 한다.
Number.isNaN 사용하기
isNaN // 느슨한 검사Number.isNaN // 엄격한 검사
경계 다루기
min- max
function genRandomNumber(min, max) {return Math.floor(Math.random() * (max - min + 1)) + min;}const MIN_NUMBER = 1const MAX_Number = 45genRandomNumber(MIN_NUMBER, MAX_NUMBER);
const MAX_AGE = 20function isAdult(age) {// 최소값, 최대값 (포함되는지 vs 안되는지)// 이상, 초과 vs 이하, 미만if (age >= 20) {}}// 상수const MIN_IN_NUMBER = 1; // IN을 사용해서 포함여부를 확인할 수 있도록?const MAX_IN_NUMBER = 45
- 최소값과 최대값을 다룬다.
- 최소값과 최대값 포함 여부를 결정해야 한다 (이상-초과 / 이하-미만)
- 혹은 네이밍에 최소값과 최대값 포함 여부를 포함한다.
begin - end
- 시작과 끝이 동일하지 않는 경우
- 예시
- airbnb의 체크인 날짜
- 체크인은 begin
- 체크아웃 end
- 수많은 datepicker 라이브러리가 begin 과 end로 구분됨
- airbnb의 체크인 날짜
function reservationDate(beginDate, endDate) {// code ...}reservationDate('YYYY-MM-DD', 'YYYY-MM-DD')
first - last
- 포함된 양 끝을 의미하는 네이밍
- 부터 ~~~ 까지
- min - max 와 비슷한데 min, max 사이에 값들이 규칙성이 없을 때 고려할 수 있는 것
const students = ['포코', '존', '현석'];function getStudents(first, last) {// code...}getStudents(students[0],'현석')
- 사이에 연속성이 없는 경우
- 예시
- html dom 요소 firstChild, lastChild
prefix - suffix
- 접두사와 접미사
- 예시
- 자바스크립트의 getter와 setter
- 리액트 Hook의
use
접두사를 사용한다.- react use 라는 파생 라이브러리가 있음 -> 다 파일명이 use prefix가 붙어있다.
- jquery $를 붙이는 약속이 있음
- 자바스크립트 클래스
#
private field -> 옛날에는_
로 표현했었음 - underscore라는 라이브러리
_
prefix를 사용한다. - lodash 도
_
prefix를 사용한다. - component도 있다.
- vue에서 컴포넌트를다룰 때
Base
App
,V
를 붙인다든지... 팀에서 코드를 작성할 때 규칙을 정함
- vue에서 컴포넌트를다룰 때
- Redux를 사용할 때
_REQUEST
,_SUCCESS
,_FAILURE
네트워크의 상태를 suffix로 표현
- file tree에서...
- 복수단위로 관리되는 파일 폴더는
_s
를 붙임,
- 복수단위로 관리되는 파일 폴더는
- 개인이나 팀에서 네이밍, 컨벤션을 정할 때
prefix
,suffix
를 사용할 수 있다. 일관성을 위한 규칙임
매개변수의 순서가 경계다
호출하는 함수의 네이밍과 인자의 순서의 연관성을 고려하기
function someFunc (someArg, someArg) {}genRandomNumber(1, 50) // 1부터 50까지 난수를 생성하나? 라고 추측할 수 있다.getDates('2021-10-11', '2021-10-32') // 저 사이 날짜를 가져오는 구나 라고 추측이 가능genShuffleArray(1, 5) // 1부터 5까지 숫자를 랜덤으로?
- 인자를 2개를 넘기면 대부분 시작과 끝을 의미로 유추할 수 있다.
- 그래서 2개가 넘지 않도록 고정하는게 좋다.
유지보수에 도움되는 함수
- 매개변수가 2개를 넘지 않도록 하기
- 인자가 2개보다 많다면?
- 객체로 받거나
...
나머지 매개변수 사용 하기arguments
객체 활용하기 JavaScript 함수, 일급 객체- 함수를 고치기 어려운 상황이라면? 래핑하는(그 함수를 호출하는) 함수를 만들기
분기 다루기
값식문
문법이 중요한 이유: 개발자는 프로그래밍 언어를 사용해서 컴퓨터를 이해시켜야 하기 때문이다. 컴퓨터는 단 하나의 문법 에러, 오타만 있어도 에러가 발생 -> 서비스 오류 하지만 값, 식, 문에 있어서 문법을 잘 지키지 않고 넘어가는 경우가 많다.
// This JSX:ReactDOM.render(<div id="msg">Hello World!</div>,mountNode)// is transformed to this JS:ReactDOM.render(React.createElement('div', { id: 'msg' }, 'Hello World!'), mountNode)
()
는 함수와 관련된다.- 값이 들어가야할 자리에 문을 넣으면 오류. ex)객체 값에
if
문이 들어가면 오류 - 값이 들어가야 할 자리에 식이 들어가는 것은 괜찮다. 식은 값으로 귀결될 수 있다.
- ex) 요소 id 값에 표현식(삼항 연산자)는 쓸 수 있다.
if
문은 쓸 수 없다.
- ex) 요소 id 값에 표현식(삼항 연산자)는 쓸 수 있다.
- 논리 연산자는 `if` 대신 사용 가능하다.
{}
안에는 값과 식만 들어가야 한다.
삼항 연산자 다루기
- 삼항 연산자를 사용하냐 안하냐보다 일관성이 중요하다.
- 오히려 삼항 연산자가 너무 복잡하면
switch case
문을 사용하는 것이 좋다.
const example = condition1? a === 0 ? 'zero' : 'positive': 'negative'
위와 같은 코드는 사람이 읽기 쉽도록 괄호로 감싸는 것이 좋다.
const example = condition1? (a === 0 ? 'zero' : 'positive'): 'negative'
반환값이 void
인 함수를 삼항연산자에 사용하면 undefined
가 들어가는 것, 의미있는 코드가 아니다.
function alertMessage(isAdult) {isAdult? alert('입장이 가능합니다.'): alert('입장이 불가능합니다.')}
개인의 취향에 따라 다르지만 강사는 이 상황에선 삼항 연산자가 어울리지 않는다고... 공감한다.
if (isAdult) {alert('입장이 가능합니다.')} else {alert('입장이 불가능합니다.')}
Truthy & Falsy
자바스크립트는 형변환을 사용자가 의도하지 않더라도 일어날 수 있다.
truthy 참 같은 값 -> 참으로 평가된다. falsy 거짓 같은 값 -> 거짓으로 평가된다.
function printName(name) {// 안전한 코드를 만들기 위해 다음과 같이 if문을...if (name === undefined || name === null) {return '사람이 없네요'}return '안녕하세요 ' + name + '님'}// 하지만 falsy를 생각하면 다음과 같이 간단하다.if (!name) {return '사람이 없네요'}
null
과 undefined
모두 falsy이기 때문에 가능한 것이다.
단축평가 (short-circuit evaluation)
단축 평가를 이용하면 분기를 줄일 수 있다.
// ANDtrue && true && '도달 O' // 도달 Otrue && false && '도달 X' // false// ORfalse || false || '도달 O' // 도달 Otrue || true || '도달 X' // true
function fetchData() {//if (state.data) {// return state.data//} else {// return 'Fetching...'//}return state.data || 'Fetching...'}
지금은 사용이 어색하지만 의식적으로 사용하다보면 언젠가는 그냥 편하게 사용할 수 있을 것!
else if 피하기
else if
를 맹목적으로 사용할 필요는 없다.
const x = 1;if (x >= 0) {// true} else if (x > 0) {} else {}// else if는 else 후에 if를 한 것과 같다는 사실을 잊지 말기if (x >= 0) {} else {if (x > 0) {}}
else if
를 사용한다는게 조건에 대해 명확하게 생각하고 있지 못하는 상황이라고 볼 수 있음.
계속 쓰는 행위를 하면 결국 switch case
를 사용하는 것이 낫다.
else 피하기
else if
와 마찬가지
function getActiveUserName(user) {if (user.name) {return user.name}// else 처리 필요 없음return '이름 없음'}
else
를 사용하면 스타일 상 문제 뿐 아니라 논리적으로 단정된 로직을 사용하는 것 ...
함수는 참이고, 거짓일 때 반환이 명백하게 짜여진 함수가 된다.
습관적으로 if - else
로직을 사용하다가 다음과 같은 실수를 할 수 있다.
function getHelloCustomer(user) {if (user.age < 20) {report(user)} else {return '안녕하세요'}}
20세미만인 사람에게 안녕하세요
를 리턴 못함
else
를 애초에 안썼다면 괜찮다.
function getHelloCustomer(user) {if (user.age < 20) {report(user)}return '안녕하세요'}
Early Return
읽어 보기 - Early Return 하는 코드를 작성하자
function loginService(isLogin, user) {if (!isLogin) {if (checkToken()) {if (!user.nickName) {return registerUser(user)} else {refreshToken()return '로그인 성공'}} else {throw new Error("No Token")}}}
위 분기로직은 매우 복잡하다. 다음과 같이 조건을 확인한다.
- 로그인 여부
- 토큰 확인
- 기등록 유저인지 확인
- 맞으면 로그인 성공
- 아니면 회원가입으로 이동
다음처럼 early return을 활용해서 함수를 미리 종료시킬 수 있다. early return 을 사용하면 코드의 흐름을 이해하기 편하다.
function loginService(isLogin, user) {// Early Returnif (isLogin) {return}if(!checkToken()) {throw new Error("No Token")}if (!user.nickName) {return registerUser(user)}login()}function login() {refreshToken()return '로그인 성공'}
부정 조건문 지양하기
부정 조건문을 추천하지 않는 이유: 실수할 가능성이 크기 때문에
- 사람이 생각할 때 답답하다.. 조건을 보고 여러번 생각해야 할 수 있다.
- 프로그래밍 언어 자체로 if문이 먼저 오고 true부터 실행시킨다.
다음 처럼 조건 앞에 !
을 추가해야 한다.
// 숫자일 때만 실행하는 로직if (!isNaN(3)) {console.log('숫자입니다')}
함수를 만들어 위임하면 생각할 것이 적어짐 덜 헷갈린다.
function isNumber(num) {return !Number.isNaN(num) && typeof num === 'number'}if (isNumber(3)) {console.log('숫자입니다')}
기본적으로 if-else
문에서 if
(참인 경우) 부터 나온다. 두번 생각할 필요 없이 참일때 실행된다고 바로 생각하면 되는데 부정조건문을 쓰면 뒤집어서 한번 더 생각해야 함
부정조건문을 사용할 때 (예외적으로)
- early return 사용 시
- form validation
- 보안 혹은 검사하는 로직
Default Case 고려하기
function sum(x, y) {x = x || 1y = y || 1return x + y}sum(100, 200)
- 사용자에게 값을 전달받지 못했을 때 기본값을 정해놓는 것이 좋음
- 엣지 케이스가 어디든지 도사리고 있을 수 있기 때문에 항상 조심하자.
- 팀에서 코어 라이브러리를 개발하는 팀에선 이런 부분을 염두해 두고 개발, 안전하고 확장성 높은 코드를 작성할 수 있다.
parseInt
의 두번째 인자 생략하지 않기 명시적으로 전달해야 한다! 기본값은 10이 아니다. (강사가 두 번 정도 강조했다. 다른 섹션에서도...)- 지금까지 몰랐다. 단순히 사용만 했지 명세를 읽어보지 않았기 때문이라는 생각이 든다.
parseInt
사용시 실수를 줄이기 위한 코드
function safeParseInt(number, radix) {return parseInt(number, radix || 10)}
명시적인 연산자 사용 지향하기
연산자 우선순위를 무조건 외우는 것보다 조금 더 안전하게 연산자 우선순위를 사용하는 방법을 알아보자.
- 괄호 사용하기
- 전위, 후위 증가,증감 연산자도 예측하기 힘들어서 헷갈리는 경우가 있음. 사용 지양하기
number-- // 보다number = number -1 // 사용하기
Redux가 추구하는 것 , 예측 가능한 상태 컨테이너
-> 예측가능한 코드를 작성해야 한다.
Nullish coalescing operator
- 숫자
0
은 falsy이기 때문에||
로 논리 평가를 하면 분리하기 어렵다.
function createElement(type, height, width) {const element = document.createElement(type || 'div')element.style.height = String(height || 10) + 'px'element.style.width = String(width || 10) + 'px'return element}createElement('div', 0, 0) // -> 10px 10px div가 생성// 0을 전달하면 falsy 이기 대문에 or 연산에서 분리가 어렵다.
다음과 같이 nullish coalescing operator를 사용하면 된다.
function createElement(type, height, width) {const element = document.createElement(type ?? 'div')element.style.height = String(height ?? 10) + 'px'element.style.width = String(width ?? 10) + 'px'return element}createElement('div', 0, 0) // -> 0px 0px div가 생성
null
과 undefined
를 사용할 때만 사용
논리 연산자와 함께 사용하면 안 된다. 괄호로 감싸면 괜찮음
드모르간의 법칙
- 조건이 복잡할 때 활용하기
// if (!(A || B)) {// }// if (!A && !B) {// }
배열 다루기
JavaScript의 배열은 객체다
그래서 배열을 다루는데 주의가 필요하다.
const arr = [1, 2, 3]const obj = {0: 1,1: 2,2: 3}if (typeof arr === 'object') {}if (arr in Array) {}if (arr.length) {} // 문자열 프로퍼티에도 length가 있어서 `length`로 배열여부를 확인해선 안 된다.
위 방법들 대신 Array.isArray()
활용하기
const arr = '[1, 2, 3]'Array.isArray(arr) // false
Array.length
const arr = [1, 2, 3]console.log(arr.length) 3arr.length = 10console.log(arr.length) // 10//arr -> [1, 2, 3, empty × 7]
length
는 배열의 길이라기보단 마지막 인덱스에 가깝다. 주의해서 의식적으로 사용하기
배열 요소에 접근하기
- 인덱스를 통해 요소에 접근하는 것은 그 인덱스의 요소가 무엇을 의미하는지 바로 이해하기가 힘들다. 그래서 구조 분해 할당을 활용하는 것이 좋다.
// 0과 1이 무엇인지 바로 알기 어렵다.function operateTime(input, operators, is) {input[0].split('').forEach((num) => {cy.get('.digit').contains(num).click()})input[1].split('').forEach((num) => {cy.get('.digit').contains(num).click()})}// 다음과 같이 나누면 이해하기 쉽다.function operateTime(input, operators, is) {const [firstInput, secondInput] = inputsfirstInput.split('').forEach((num) => {cy.get('.digit').contains(num).click()})secondInput.split('').forEach((num) => {cy.get('.digit').contains(num).click()})}// 더 간단하게 하기 인자로 받을 때 부터 구조분해할당function operateTime([firstInput, secondInput], operators, is) {firstInput.split('').forEach((num) => {cy.get('.digit').contains(num).click()})secondInput.split('').forEach((num) => {cy.get('.digit').contains(num).click()})}operateTime([1, 2], 1, 2)
배열 요소가 하나만 있어도 구조분해할당이 가능하다.
function formatDate(targetDate) {const [date] = targetDate.toISOString().split('T')const [year, month, day] = date.split('-')return `${year}년 ${month}월 ${day}일`}
유틸 함수를 사용해도 된다. lodash 처럼 함수 만들기
function head(arr) {return arr[0] ?? ''}
유사 배열 객체
const arrayLikeObject = {0: 'HELLO',1: 'WORLD',length: 2}const arr = Array.from(arrayLikeObject) // ['HELLO', 'WORLD']Array.isArray(arrayLikeObject) // falseArray.isArray(arr) // true
유사 배열 객체 는 Array.isArray
를 사용해서 확인하면 false
다.
NodeList
arguments
- 함수 내부에 갖고 있는 유사배열객체
- 고차 함수가 동작하지 않는다.
Array.from
으로 배열로 바꿔주면 동작
불변성
const originonArray = ['123', '456', '789']const newArray = originArrayoriginArray.push(10)originArray.push(11)originArray.push(12)originArray.unshift(0)
위 코드를 실행해보면, originArray
를 변경시켰는데 newArray
도 영향을 받는다.
불변성 지키는 방법
- 배열을 복사한다.
- 새로운 배열을 반환하는 메서드들을 활용한다. -> 고차 함수
const newArray = [...originArray]
for 문 배열 고차 함수로 리팩터링
배열 고차 함수를 사용하면 선언적 프로그래밍이 가능해진다.
배열 메서드 체이닝 활용하기
배열 고차 함수는 결과 배열을 다시 배열 메서드에 사용할 수 있다.
map vs forEach
map
은 새 배열을 반환한다.forEach
는 반환하지 않는다. 그저 순회만 한다.- 그렇다고
map
만 사용할 필요는 없다. 무언가 반환이 필요 없다면forEach
를 사용해도 된다. - 명세에 맞게 사용하기
Continue & Break
배열 고차 함수에서는 continue
와 break
가 먹히지 않는다.
그래서 조기에 반복문 종료가 가능한 메서드들을 잘 조합해서 흐름 제어도 가능하다.
- 조기에 반복문 종료가 가능한 메서드
for
,for in
,for of
사용하기every
,some
,find
,findIndex
객체 다루기
Shorthand Properties
const counterApp = combineReducers({counter,extra,})
const firstName = 'poco'const lastName = 'jang'const person = {firstName,lastName,getFullName() {return this.firstName + ' ' + this.lastName}}
- 소스코드를 보고 단축 속성을 사용했는지 아닌지 확인할 수 있다.
- 리팩터링할 때 일관성있게 작업하기
Computed Property Name
const [state, setState] = useState({id: '',password: '',})const handleChange = (e) => {setState({[e.target.name]: e.target.value // computed property name})}return (<React.Fragment><input value={state.id} onChange={handleChange} name="name" /><input value={state.password} onChange={handleChange} name="password" /></React.Fragment>)
리액트에서 제공하는 기능인지, 자바스크립트 코드인지 헷갈려서 실수하는 경우가 많다.
const noop = createAction('INCREMENT')const reducer = handleActions({[noop]: (state, action) => ({ // 메서드counter: state.counter + action.payload,})},{ counter: 0 },)
라이브러리 도구 사용시, 자바스크립트 문법을 잘 알아야 코드를 읽기 쉽다.
Lookup Table
순람표
- 배열 데이터 구조에서 키와 밸류로 나열된 표
if
문이 너무 길어질 경우에switch case
문 사용하기
function getUserType(type) {switch (key) {case 'ADMIN':return '관리자'case 'INSTRUCTOR':return '강사'case 'STUDENT':return '수강생'default:return '해당 없음'}}
Coumputed property name 로 변경
function getUserType(type) {const USER_TYPE = {ADMIN: '관리자',INSTRUCTOR: '강사',STUDENT: '수강생',UNDEFINED: '해당 없음'}return USER_TYPE[type] ?? USER_TYPE['UNDEFINED']}console.log(getUserType('Hello'))
function getUserType(type) {return ({ADMIN: '관리자',INSTRUCTOR: '강사',STUDENT: '수강생',}[type] ?? '해당 없음')}console.log(getUserType('Hello'))
Object Destructuring
매개변수의 순서가 정해져 있는 것은 문제가될 수 있다.
function Person(name, age, location) {this.name = namethis.age = agethis.location = location}
구조 분해 할당 사용하기 매개변수의 순서가 바뀌어도 상관없고 매개변수에 원치 않는 값은 넘기지 않아도 되어서 좋다.
function Person({ name, age, location }) {this.name = namethis.age = agethis.location = location}const poco = new Person({name: 'poco',age: 33,location: 'korea'})
특정 인자를 필수로 받아야하는 경우엔 다음과 같이 할 수 있다.
function Person(name, { age, location }) {this.name = namethis.age = agethis.location = location}const poco = new Person(name, {age: 33,location: 'korea'})
배열의 길이가 길 경우
const orders = ['First', 'Second', 'Third']const st = orders[0]const rd = orders[2]// 위 처럼 인덱스로 가져오는 대신const [first, , third] = orders// 또는// 배열의 길이가 길면 아래와 같이 사용하는 것이 좋다.const { 0: st2, 2: rd2 } = ordersconsole.log(st2) // Firstconsole.log(rd2) // Third
Object.freeze
const STATUS = Object.freeze({PENDING: 'PENDING',SUCCESS: 'SUCCESS',FAIL: 'FAIL',})STATUS.NEW_PROP = 'P2' // 추가되지 않는다.// 동결 확인Object.isFrozen(STATUS.SUCCESS) // true
shallow copy vs deep copy
Object.freeze
는 shallow
const STATUS = Object.freeze({PENDING: 'PENDING',SUCCESS: 'SUCCESS',FAIL: 'FAIL',OPTIONS: {GREEN: 'GREEN',RED: 'RED',}})// 동결 확인Object.isFrozen(STATUS.OPTIONS) // falseSTATUS.OPTIONS.GREEN = 'G' // 가능STATUS.OPTIONS.YELLOW = 'Y' // 가능delete STATUS.OPTIONS.RED // 가능
중첩 프리징을 하는 법
- 대중적인 유틸 라이브러리 (lodash)
- 직접 유틸 함수 생성 (개인/팀내에서)
- 객체를 순회
- 값이 객체인지 확인
- 객체면 재귀
- 그렇지 않으면
Object.freeze
- stackoverflow
- TypeScript 사용하기 -> readonly
Prototype 조작 지양하기
class Car {constructor(name, brand) {this.name = namethis.brand = brand}sayName() {return this.brand + '-' + this.name}}const casper = new Car('캐스퍼', '현대')
프로토타입을 지양해야 하는 이유
- 자바스크립트가 발전했기 때문에 -> Class 가 생김
- 추가 기능은 직접 만들어서 모듈화 => 배포 (NPM)
- 자바스크립트의 내장 객체(Built-in Object)를 건드리지 말자. 굉장히 위험한 행동
hasOwnProperty
const person = {name: 'hhhh'}person.hasOwnProperty('age') // falseperson.hasOwnProperty('name') // true
const foo = {hasOwnProperty: function () {return 'hasOwnProperty'},bar: 'string'}foo.hasOwnProperty('bar') // hasOwnPropertyObject.prototype.hasOwnProperty.call(foo, 'bar') // true// 매번 적기 귀찮으니 함수로 호출하기function hasOwnProp(targetObj, targetProp) {return Object.prototype.hasOwnProperty.call(targetObj, targetProp)}
알아두면 좋다. 언젠가는 코드 작성시에 의미있게 도움이 될 수 있음
call
을 사용하는 대신 Object.hasOwn()을 사용하자!
직접 접근 지양하기
const model = {isLogin: false,isValidToken: false,}function login() {model.isLogin = truemodel.isValidToken = true}function logout() {model.isLogin = falsemodel.isValidToken = false}
객체에 직접 접근하지 않고 따로 함수로 빼기
- 객체를 수정하는 영역의 레이어를 따로 분리하는 것
- 추상화하는 것
- 코드를 안전하게 변화시킬 수 있다.
- 객체의 변화가 어디서 일어나는지 예측이 가능해진다.
// 객체에 대신 접근function setLogin(bool) {model.isLogin = bool}function setValidToken(bool) {model.isValidToken = bool}// 객체에 직접 접근x, 제공된 함수 사용하기function login() {setLogin(true)setValidToken(true)}
getter
와 setter
라는 접근자도 제공된다.
Optional Chaning
Extends & Mxin
함수 다루기
함수, 메서드, 생성자
// 함수function func() {return this}// 객체의 메서드const obj = {method() {return this}}// 생성자 함수// Class가 나와서 사용할 일이 잘 없다. 하지만 누군가는 사용하고 있으니 알아두기function Func() {return this}
자바스크립트의 함수는 1급 객체이기 때문에 변수나 데이터에 담을 수 있다. 매개변수로 전달 가능 (콜백 함수) 함수가 함수를 반환 (고차 함수)
함수와 this
어떤 함수냐에 따라 this가 가리키는 값이 다르다.
- 함수의
this
는 전역 객체 - 메서드의
this
는 호출한 객체 - 생성자 함수의
this
는 생성될 인스턴스
argument & parameter
- 함수를 정의할 때는 parameter
- 함수를 사용하고 있는 곳에서 argument
- 함수의 paramter는 함수의 정의 부분에 있는 이름들
- 함수의 argument 함수로 전달되는 진짜 값
- Paramter (Formal Parameter)
- 형식을 갖춘 매개변수
- Argument (Actual Parameter)
- 실제로 사용되는 인자
복잡한 인자 관리하기
인자의 갯수는 갯수에 따른 맥락을 유추할 수 있을 정도로 지정해야 한다.
function toggleDisplay(isToggle) {}function sum(sum1, sum2) {}function genRandomNumber(min, max) {}function timer(start, stop, end) {}function genSquare(top, right, bottom, left) {}
복잡한 인자 객체로 주고받기 ES2015 부터는 디스트럭쳐링 사용하기
function createCar({ name, brand, color, type }) {return {name,brand,color,type}}
Default Value
함수를 안전하게 사용하는 방법 중 하나는 인자의 기본값 정하는 것
function createCarousel({ margin = 0, center = false, navElement = 'div' } = {}) {return {margin,center,navElement}}console.log(createCarousel())
함수를 넘겨서 필수 매개변수를 받을 수도 있다.
const required = argName => {throw new Error(`required is ${argName}`)}function createCarousel({margin = required('margin'),center = false,navElement = 'div'} = {}) {// ... some code}
Rest Parameters
가변인자를 받을 경우에 나머지 매개변수를 사용할 수 있다.
원래 arguments
객체를 사용하면 되는데, 추가적인 인자를 받을 수 없는 문제가 있다.
Rest Parameter로 들어온 값은 배열이기 때문에 배열 메서드를 사용할 수 있다.
function sumTotal(...args) {return args.reduce((acc, curr) => acc + curr)}
추가적인 인자도 받을 수 있다.
function sumTotal(init, ...args) {console.log(init)return args.reduce((acc, curr) => acc + curr)}
void & return
function handleClick() {return setState(false)}function showAlert(message) {return alert(message)}
문제가 없다고도 볼 수 있지만 이런 코드는 줄여 나가야 한다.
기본적으로 setState
와 alert
은 void
함수, 굳이 return
을 넣을 필요가 없다. 의도가 불분명해짐...
자바스크립트는 리턴 값이 없을 때 undefined
를 리턴한다.
불필요한 리턴을 사용하는 것은 내가 사용하는 api들의 리턴값 여부를 확인하는 습관이 덜 되었을 수 있다는 것
리턴이 있을법한 네이밍
is
,get
화살표 함수
화살표 함수를 무조건 사용할 필요도 없고 사용하지 않을 이유도 없다.
화살표 함수를 사용하지 않을 이유 ?
- 화살표 함수를 메서드로 사용할 때 화살표 함수의
this
는 전역 공간을 가리킨다. 화살표 함수는 렉시컬 스코프를 가지게 된다. 호출된 객체를this
로 바라보는 것이 아닌 렉시컬 스코프로 상위 문맥을 따른다.
const user = {name: 'Poco',getName: () => {return this.name}}
- 화살표 함수는
arguments
,call
,apply
,bind
를 사용할 수 없다.
arguments
를 사용하고 싶다면 나머지 매개변수를 사용하면 된다.
화살표 함수는 생성자 함수로 사용할 수 없다.
클래스를 다룰 때 화살표 함수는 생성자 함수 내에서 초기화가 되어버린다. 그래서 자식 클래스에서 사용할 수 없다.
class Parent {parentMethod() {console.log('parentMethod')}parentMethodArrow = () => {console.log('parentMethodArrow')}overrideMethod = () => {return 'Parent'}}class Child extends Parent {childMethod() {super.parentMethod()}overrideMethod() {return 'Child'}}const child1 = new Child().overrideMethod(); // Parent// 부모의 메서드가 호출된다. 화살표가 아닌 일반 함수로 만들면 오버라이드 된다.
- 화살표 함수는
yield
키워드와 함께 사용할 수 없다.
앞서 나온 이유들로 인해 맹목적으로 사용하면 오류를 만날 수도 있다.
Callback Function
콜백 함수는 비동기를 제어하는 기법이라고만 생각하는 경우가 간혹 있다. 하지만 함수의 제어권을 다른 함수에 위임하는 기능도 있다.
function register() {const isConfirm = confirm('회원가입에 성공했습니다.')if (isConfirm) {redirectUserInfoPage()}}function login() {const isConfirm = confirm('로그인에 성공했습니다.')if (isConfirm) {redirectIndexPage()}}
위 코드에서 confirm
에 전달하는 메시지도 다르고, 확인 후에 실행되는 함수도 서로 달라서 두 개의 함수가 만들어져 있다. 이런 경우 리팩터링을 어떻게 할 수 있을까? -> callback function 사용하기
function confirmModal(message, cbFunc) {const isConfirm = confirm(message)if(isConfirm && cbFunc) {cbFunc()}}function register() {cconfirmModal('회원가입에 성공했습니다.', redirectUserInfoPage)}function login() {cconfirmModal('로그인에 성공했습니다.', redirectIndexPage)}
콜백 함수로 함수를 위임했다.
순수 함수
순수 함수란 side effects를 만들지 않는 함수를 말한다.
side effects
- Logging a value to the console
- Saving a file
- Setting an async timer
- Making an AJAX HTTP request
- Modifying some state that exists outside of a function, or mutating arguments to a function
- Generating random numbers or unique random IDs (such as
Math.random()
orDate.now()
)
let num1 = 10let num2 = 20function impureSum1() {return num1 + num2}function impureSum2(newNum) {return num1 + newNum}
순수 함수는 호출할 때마다 일관적인 값을 반환해야 하는데 위 impureSum1
은 함수 외부의 변수가 변경될 때 값이 변경된다. 즉 비순수 함수다. impureSum2
도 마찬가지이다.
이렇게 사이드 이펙트가 있다면 코드를 예측하는 것이 어려워진다.
순수 함수는 다음 코드와 같이 side effects를 만들지 않는 함수이다.
function pureSum(num1, num2) {return num1 + num2}
const obj = { one: 1 }function changeObj(targetObj) {targetObj.one = 100return targetObj}changeObj(obj)obj // { one: 100 } , 변경되어 있다! 비순수 함수
객체나 배열을 조작하는 함수를 만들 때는 새로운 객체와 배열을 리턴해줘야 한다.
const obj = { one: 1 }function changeObj(targetObj) {return {...targetObj, one: 100 }}changeObj(obj)obj // { one: 1 } , 원래 객체의 값은 그대로, 순수 함수
항상 순수 함수를 만든다는 의식적인 생각을 가지고 코드를 작성하는 것이 좋다.
자바스크립트 레퍼런스 타입을 사용하는 경우에는 항상 새 값을 반환하는 함수를 만들자!
Closure
function add(num1) {return function sum(num2) {return num1 + num2}}const addOne = add(1)(3) // 4const addTwo = add(2)addTwo(1) // 3
클로저 코드를 볼 때, 괄호의 갯수를 보고 그 괄호들이 모두 호출되면 완벽히 함수가 호출되고 종료된다고 보면 된다.
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)const sumMultiple = addOne(multiple)
sumAdd
와 sumMultiple
는 addOne
이라는 함수를 똑같이 호출해도 각자의 컨텍스트를 가지고 있다.
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)arr.filter(isString)
클로저를 활용하면 중복되는 코드를 하나로 합칠 수 있다.
function isTypeOf(type) {return function(value) {return typeof value === type}}const isNumber = isTypeOf('number')const isString = isTypeOf('string')arr.filter(isNumber)arr.filter(isString)
클로저를 사용하는 또 다른 예시
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://~')const daumApi = fetcher('http://~')naverApi.fetch('/webtoon').then(res => res)daumApi.fetch('/search').then(res => res)
someElement.addEventListener('click', debounce(handleClick, 500))someElement.addEventListener('click', throttle(handleClick, 500))
Todo
- 무엇때문에 이렇게 동작을 하는걸까 생각해보기
- 코드를 작성할 때 의도적으로 클로저를 사용하면서 코드를 리팩터링 해보기
- 단점이 무엇인지, 메모리가 어떤 문제점을 겪는지
- 메모이제이션 관련 코드 작성해보기