SQL은 테이블 간 관계를 형성하는 데이터 베이스 운영 방식이다. 사전에 데이터베이스 설계 규칙에 근거해서 엄격한 테이블 구조 정의를 하는 게 필요하고, 실제 사용할 때에도 해당 설계를 고려해 관계형으로 데이터를 join해 사용한다.
SQL을 지원하는 데이터베이스로는 MySQL, PostgreSQL 등이 있다.
SQL이 아닌 모든 데이터베이스는 NoSQL이다. SQL이 No, 즉 '아니다'라는 뜻인 만큼, 이름부터가 상당히 직관적이다.
NoSQL은 사전에 엄격한 테이블 구조에 대한 설계를 갖추지 않고, 좀 더 유연하게 사용할 수 있다.
보통은 하나의 테이블에 모든 데이터를 밀어넣는 방식으로 사용하기 때문에 상대적으로 사용이 수월하다.
대표적인 NoSQL 데이터베이스에는 MongoDB와 Redis 등이 있다.
MongoDB를 설치하고 mongo를 터미널에 실행하면 mongo shell을 사용할 수 있다. Node를 REPL 환경에서 인라인으로 실행하는 것과 유사하다.
mongo shell에서 db를 입력하면 test 데이터베이스가 확인된다. MongoDB에서 기본으로 제공해 주는 데이터베이스다.
show databases 또는 show dbs를 치면 생성된 데이터베이스를 확인할 수 있다. 예시에선 test 데이터베이스가 확인된다.
데이터베이스를 추가하려면 use {데이터베이스 이름}을 입력해주면 된다. use animalShelter라고 입력하면 새로운 데이터베이스가 생성되고, db를 추가로 입력하면 생성된 animalShelter가 참조되는 걸 확인할 수 있다.
use는 git 명령어 중 branch와 유사해서 데이터베이스가 없으면 생성해 주고, 데이터베이스가 있으면 해당 데이터베이스로 이동해 준다.
하지만, show dbs를 했을 때 새롭게 생성한 animalShelter 데이터베이스가 보이는 게 아니라 기존 test 데이터베이스가 그대로 보이게 된다. 이유는, MongoDB는 새롭게 생성된 데이터베이스에 새로운 데이터가 추가되기 전까지는 해당 데이터베이스에 대한 정보를 보여주지 않기 때문이다.
mongo shell에 표시된 명령어와 응답들을 지워주려면 clear 명령어가 아니라 cmd + k 단축키를 사용해야 한다. clear는 터미널의 명령어고, mongo shell에선 지원하지 않는다.
MongoDB는 BSON이라는 데이터 타입을 사용한다. BSON은 Binary JSON의 줄임말로, JSON 데이터 타입을 이진법 형태로 표현한 형식이다.
JSON은 Date 타입 등 일부 데이터 타입을 처리할 수 없지만, BSON은 좀 더 많은 데이터 타입을 처리해 줄 수 있고, 또한 메모리 공간도 더 적게 차지한다는 장점이 있다.
use animalShelter로 새로운 데이터베이스를 만들어줬다면, 데이터베이스에 콜렉션을 추가해 줄 수 있다. 콜렉션은 관계형 데이터베이스에서 하나의 테이블로 치환되는 개념이며, 콜렉션은 다수의 도큐먼트(Document)의 집합으로 이루어진다. 도큐먼트는 데이터베이스에서 관리하는 하나의 데이터를 의미한다.
사용할 데이터베이스로 이동한 상황에서 db.{콜렉션 이름}.insert를 사용해 주면 도큐먼트들을 콜렉션에 추가해 줄 수 있다.
예를 들어, db.dogs.insert를 해주면 dogs라는 콜렉션이 없을 경우 새로운 콜렉션을 추가한 다음 할당해준 값을 추가해주며, 콜렉션이 있으면 기존에 생성한 콜렉션에 도큐먼트를 추가해 준다.
MongoDB의 커맨드는 자바스크립트 문법과 유사하기 때문에, 객체로 값을 넣을 때 JSON처럼 키에 ""를 씌워줄 필요가 없다. 일반 자바스크립트 객체처럼 넣어주면 알아서 값을 BSON 타입으로 파싱해서 사용한다.
db.dogs.insert([{name: 'coco', age: 3}, {name: 'toto', age: 5}]) 형태로 데이터를 넣어주면 dogs 콜렉션에 두 개의 도큐먼트를 추가된다. 배열이 아니라 단일 객체로 값을 넣으면 하나의 도큐먼트만 추가된다.
콜렉션의 데이터를 조회할 땐 db.{콜렉션 이름}.find()를 사용한다. 매개 변수로 아무것도 전달하지 않으면 콜렉션에 포함된 모든 도큐먼트를 가져온다.
db.{콜렉션 이름}.findOne()을 실행하면 전체 도큐먼트 중 하나의 도큐먼트만을 반환한다.
find 메서드 안에는 조건을 넣어줄 수도 있다. 예를 들어, db.dogs.find({ catFriendly: true })라고 조회하면 catFriendly라는 속성이 true인 도큐먼트만 결과로 반환된다.
즉, 몽고DB에선 find 메서드의 매개 변수로 조건을 전달해서 원하는 값을 쿼리 할 수 있다. 쿼리 조건에서는 논리 연산, 범위 확인 등 다양한 조건문이 사용될 수 있다.
도큐먼트의 필드가 nesting 된 객체일 경우, 조건을 걸어서 찾기 위해 {'personality.catFriendly': true}와 같은 식으로 조건을 사용할 수 있다. 이렇게 사용하면 personality: {catFriendly: true}인 도큐먼트를 찾아준다. 이때, 객체의 점 표기법처럼 사용하는 구문은 '' 또는 ""로 감사서 문자열처럼 인식되게 해 줘야 됨을 주의하자.
비교 연산을 할 땐 db.dogs.find({age: {$gt: 8}})와 같은 방식으로 사용할 수 있다. $gt는 greater than의 줄임말이다. '~보다 더 작은' 값을 찾으려면 $lt(less than)을 사용한다. 크거나 같을 경우는 $gte, 작거나 같을 경우는 $lte를 사용한다. $eq는 같은 경우, $ne는 같지 않은 경우를 나타낸다.
$in은 특정 필드가 지정한 배열 안의 값에 해당할 경우를 의미한다. db.dogs.find({breed: {$in: ['Mutt', 'Corgi']}})를 사용하면 breed가 Mutt이거나 Corgi인 경우인 도큐먼트를 찾는다. $nin은 'not in'의 줄임말로, $in의 반대로 동작한다.
조건은 find({})의 객체 안에 ,로 구분해서 나열해 줄 수 있다. 조건을 나열하면 자연스럽게 &&로 해당하는 경우가 찾아진다.
db.dogs.find({$or: [{'personality.catFriendly': true}, {age: {$lte: 2}}]})처럼 사용하면 배열 안의 두 조건이 ||으로 연결된 논리 연산이 된다. $or, $nor, $and, $not 등도 사용 가능하다.
도큐먼트를 수정할 땐 db.{콜렉션 이름}.updateOne 또는 db.{콜렉션 이름}.updateMany가 사용된다. updateOne은 조건에 맞는 하나의 도큐먼트에 대해 지정된 값으로 수정해 주고, updateMany는 2개 이상의 도큐먼트에 같은 수정 사항을 적용해 준다.
db.dogs.updateOne({name: 'Charlie'}, {$set: {age: 4}})으로 작성해 주면 이름이 Charlie인 강아지를 찾아 나이를 4로 업데이트해 준다.
이때, 업데이트하는 구문에 {$set: /* 업데이트 할 내용 */} 형태로 사용돼야 함을 주의하자.
업데이트 할 내용은 꼭 하나만 들어가야 하진 않고, 두 개 이상이 들어갈 수도 있다. { $set: { age: 4, isAdopted: false } }처럼 작성하면 age와 isAdopted 모두 값이 변경된다.
만약에 현재 도큐먼트에 없는 속성을 { $set: { breed: 'poodle' } } 형태로 추가해 주면 도큐먼트에 속성과 값이 추가된다. 업데이트의 개념이 '전체 도큐먼트 중 특정 부분이 바뀐다'로 이해했을 땐 자연스러운 동작이다.
db.dogs.updateOne({name: 'Charlie'}, {$set: {age: 6}, $currentDate: {lastModified: true}}) 처럼 작성하면 $currentDate를 통해 마지막으로 업데이트된 시점의 타임스탬프를 lastModified라는 필드 이름으로 저장해 둘 수 있다.$currentDate는 객체에 있는 키가 true로 설정되면 해당 이름을 필드명으로 하는 타임스탬프를 찍는다.
도큐먼트를 삭제할 땐 db.{콜렉션 이름}.deleteOne 또는 db.{콜렉션 이름}.deleteMany를 사용한다. deleteOne은 조건에 해당하는 하나의 도큐먼트를 삭제하고, deleteMany는 조건에 해당하는 모든 도큐먼트를 삭제한다.
만약에 콜렉션의 모든 도큐먼트를 삭제하고 싶다면 db.{콜렉션 이름}.deleteMany({})로 사용해 주면 된다. 빈 객체를 사용하면 콜렉션에 있는 도큐먼트가 몇 개든 상관없이 전부 삭제한다.
React render 함수, useState 구현하기
addEventListener로 이벤트를 부착할 땐 같은 요소에 같은 타입의 핸들러를 중복해서 붙일 수 있다. 그러면 한 번의 이벤트 발생에 대해서도 여러 번 이벤트 처리 함수가 동작할 수 있게 되고, 성능 상 여러 문제가 발생할 수 있다. 이벤트 핸들러를 등록했으면 중복되지 않게 잘 지워주는 것도 신경 써주자.
Fragment를 나타내는 태그를 documnet의 createDocumentFragment를 사용하면 만들어줄 수 있다. 실제 DOM에 하위 요소들을 연결해줘야 하는데 각각 개별로 연결해 주면 DOM의 업데이트가 좀 더 자주 발생하지만, document.createDocumentFragment로 만들어준 태그에 연결해 준 다음 실제 DOM에 붙여주면 한 번만 DOM이 업데이트된다. 이런 상황에선 성능을 생각해서 사용을 고려해 볼 만하다.
요소의 하위 요소가 leaf node인 텍스트로 예상되는 상황에서 $elem.textContent = textContent 이런 식으로 사용을 주로 했었는데, 가급적 const textNode = document.createTextNode로 DOM 요소를 따로 만들어준 다음에 $elem.appendChild(textNode) 식으로 연결해 주자. 조금 더 자연스럽고 안전한 방식이다.
문자열의 prefix를 추출해 사용하고 싶을 때 str.slice(0, 2) 이렇게 원하는 구간을 잘라서 사용할 수도 있지만, str.startsWith 메서드를 사용할 수도 있다. str.startsWith(2)와 같이 인자로 원하는 길이를 넘겨주면 그 길이만큼 시작하는 구간의 문자열을 확인해 준다.
문자열을 인덱싱할 때 slice도 사용 가능하지만, substring도 사용 가능하다. 사용 방법은 거의 유사하나, slice는 음수 인덱스가 활용 가능하다는 점, substring은 첫 번째 인자가 두 번째 인자보다 클 때 값을 switching 해서 인덱싱 해준다는 점 등 일부 차이가 있긴 하다. 차이를 이해하고 상황에 맞게 잘 사용하자.
리액트의 useState로 생성한 state는 값을 직접 변경하면 컴포넌트 간 상태의 참조가 끊기고, 리렌더링도 발생하지 않는다는 특징을 가지고 있다. 이러한 특징을 반영하는 식으로 useState를 만들기 위해서 외부로 공개되는 state는 클로저로 관리되도록 하고, 값의 직접 변경이 허용되지 않게 즉시 실행 getter 함수로 감싸는 식으로 구현했다.
컴포넌트 리렌더링 시 useState도 결국 재호출 된다. 재호출마다 상태 초기값이 계속 재할당되면 상태는 이전 값을 기억하지 못하고 문제가 생기게 될 것이다. 이런 상황을 고려해서, useState가 리렌더링으로 인해 재호출 될 시에는 상태 초기화를 방지해 주도록 별도 처리가 필요하다.
리렌더링을 할 때, 결국 render 함수를 재호출 해야 한다. 그런데, 리렌더링 할 부분이 어딘지 알 수 없기 때문에, 일단은 컴포넌트의 최상단이 되는 App을 useState 모듈에서 불러와서 인자로 전달해 줬다. 아마도 이후에 Virtual DOM의 Diffing 알고리즘을 구현할 때 해당 부분을 변경이 필요한 부분을 Virtual DOM에서 파악해서 해당 부분만 가지고 변경을 처리해 주는 식으로 바꾸는 게 필요할 것 같다.
예외 처리가 필요한 부분이 어딘지 조금 더 꼼꼼하게 확인해서 보강해 보자.
모던 자바스크립트 딥다이브 스터디 준비
자바스크립트는 싱글 스레드이지만, 런타임인 브라우저와 Node가 멀티 스레드를 제공해 주기 때문에 동시성-병렬성 처리가 가능하다(정확히 말하면, Node와 브라우저 모두 다 메인 스레드는 싱글 스레드지만, 백그라운드에서 멀티 스레드로 동작 가능하다).
비동기 작업이 있을 때 해당 작업들이 런타임 프로그램-프로세스의 백그라운드로 가서 멀티 스레드로 처리되는 것이 '병렬 처리'의 대표적인 상황이다. 예를 들어서, setTimeout이라는 비동기 함수가 실행되면 두 번째 인자로 전달한 ms만큼 시간을 카운트하는 걸 백그라운드에서 병렬로 처리해 주고(시간을 카운트해 주는 동안에도 메인 스레드인 자바스크립트 엔진은 계속 실행 컨텍스트를 돌리고 있다), 병렬 처리가 완료되면 등록돼 있던 콜백 함수를 태스크 큐로 밀어 넣어준다.
태스크 큐는 이벤트 루프를 통해 관리되는 작업 대기열이다. 별도 스레드에서 관리되는 게 아닌, 싱글 스레드인 자바스크립트 엔진에서 관리되는 가상의 자료 구조이다.
이벤트 루프는 싱글 스레드인 콜 스택과, 멀티 스레드로 동작하는 백그라운드를 계속 반복해서 확인한다. 만약 백그라운드에서 병렬 처리로 돌아가던 작업이 완료되면 해당 작업 완료에 대한 콜백 함수를 태스크 큐로 옮겨주고, 콜 스택의 실행 컨텍스트가 비워질 경우 태스크 큐의 작업들을 선입선출 방식으로 빼내서 콜 스택에 밀어 넣어준다.
이렇게, 이벤트 루프가 두 개 이상의 스레드(콜 스택과, 브라우저-Node에서 제공하는 백그라운드)를 계속 왔다 갔다 확인하면서 백그라운드 작업 완료에 대한 처리를 해주는 것은 '동시성 처리'에 해당한다고 볼 수 있다.
자바스크립트가 싱글 스레드라는 건 개발자가 제어할 수 있는 스레드가 하나라는 의미이다. 위의 작업들을 처리하는 데 있어서 개발자가 직접 조작 가능한 영역은 콜 스택에 해당하는 부분까지이고, 나머지 이벤트 루프와 브라우저-Node의 백그라운드에서 돌아가는 멀티 스레드 작업들에 대해서는 직접 조작이 어렵다(Node.js 환경을 이용하면 일부 가능하다고 듣긴 했다).
또한, 자바스크립트의 비동기 처리는 '이벤트 기반'이라는 특징이 있다. 어떠한 이벤트가 발생하면, 그 이벤트에 대한 작업을 백그라운드로 보내고, 또 어떤 이벤트 발생 시 백그라운드 작업을 태스크 큐로 끌어온다.