우아한 테크러닝 3기

✔ 우아한 테크러닝 3기: React & TypeScript 2회차

seungmin2 2020. 9. 7. 22:45

✌ 9월 3일 우아한 테크러닝 3기 2회차 정리

🚀 2회차 강의 목표

  • JavaScript에 대한 지식을 A~Z까지 할 수 있는 만큼 해보기! (70%)
  • JavaScript의 아주 기초적인 내용보단 전체적인 흐름을 타면서 스펙들을 쭉 훑어보기
  • 상태관리자인 Redux를 직접 만들어보기! 만들어보면서 어떤 기술로 이루어져있는지! (30%)

🚀 JavaScript 훑어보기

  • 변수에 이 이라는 것을 넣을 수 있다.

    var x = 10; //x: 변수 10: 값
    let y = 10;
    const z = 10;

🌈 값에 대해서

📌 값에 대해서 굉장히 강조를 하셨다.

  • 아주 바닥의 절대불변의 원칙같은 것들을 세워놓고 (ex. 1=1, 1+1=2 불변(공리)) 이런 원칙을 충분히 이해하면 아무리 복잡한 것이라도 그 원칙으로 하나씩 벗겨나가면 굳이 안 배우고도 이해하고 그것들을 파악해 나가는 장점이 있는데 프로그래밍 언어도 마찬가지이다.

🌈 JavaScript에서의 값

  • 자바스크립트에서는 값이라고 정의한 녀석들은 변수에 넣을 수 있다.
  • 그렇다면, 문자열도 값이라고 하니까 변수에 넣을 수 있고, 숫자, 객채도 넣을 수 있다.
  • 함수도 자바스크립트에서 값이라고 규정했기 때문에 당연히 함수도 값으로써 변수에 넣을 수 있다. 그렇기 때문에 자바스크립트에서는 거의 모든 값이 값이다.

🌈 JavaScript에서의 함수

  • 자바스크립트의 함수는 반드시 값을 반환하게 되어있다. (undefinded라도..)

    function foo(){
      return 0;
    }

    📌 값을 반환하는 두가지 방법

  1. 명시적으로 리턴하는 방법.(return이 없으면 undefinded 반환)

  2. 호출할 때 앞에 new 연산자로 호출하면 이 함수는 new 연산자의 작용으로 명시적 리턴이 없어도 객체를 반환하는 인스턴스 객체

    new foo();
    let y = foo;

    🌊 JavaScript 함수가 굉장히 유연함(어려운) 점은?

  • 함수 선언문(function declaration) (세미콜론이 없다.)

  • 함수 표현식(function expression)(세미콜론이 붙는다.)

    // 함수 선언문(함수 정의문)
    function foo(){}
  • 함수 표현식은 값이라고 취급했기(변수에 넣을 수 있다.) 때문에 세미콜론이 붙을 수 있다.

    // 함수 표현식
    const bar = function bar() {};
    // bar안에 함수가 존재하니까 호출이 가능하다.
    bar();
  • 이때 이 함수가 이름을 가져도 함수 바깥쪽에서는 이 안 쪽 bar를 모른다. 그래서 const bar변수가 함수 이름을 대신한다.

  • 그러힉 떄문에 안쪽 선언된 bar는 의미가 없어서 보통 이름 없는 함수로 사용하여 생략이 가능하다.

  • 표면적으로는 이렇게 생각할 수 있지만 정확히는 함수를 값으로 취급할 땐 이름을 생략할 수 있다라고 말한다.

  • 함수가 값으로 취급해서 변수에 넣고 있기 때문에 이름을 생략할 수 있는 것이다.

    const bar = function () {};
  • 만약 단독으로 이름 없는 함수를 사용한다면 에러가 발생한다.

    function () {}
  • 이 상황은 함수를 값으로 취급하는 상황이 아니기 때문이다.

  • 그렇다면 내가 이 함수를 값으로 취급하고 싶을 땐 명시적으로 자바스크립트 엔진에게 알려줘야 한다.

  • 보통 자바스크립트는 값을 만들 때 괄호를 쓰게 되어있다.

    (function () {})
  • 하지만 이 상황은 함수를 만들기만 하고 넣지 않은 상태이다. 이유는 함수는 이름이 있어야 호출할 수 있기 때문이다.

  • 단 한가지 방법은 만들자마자 즉시 호출하는 방법이다.

    (function () {})();
  • 이것이 _Immediate Function Call_이라고 해서 만들자마자 호출이 되고 호출이 끝나고 리턴이 되면 이름이 없기 때문에 다시 호출할 수 없는 것이다.

  • 프로그래밍 상에서 단 한 번만 호출하고 싶은 상황이 플요할 때 이런 방법을 사용한다.(초기화 코드)

🌊 콜백함수와 합성 함수(Higher-Order Function, 일급 함수)

  • 위에서의 개념을 확장하여 변수가 어디 등장하는지 생각해보자.

  • 변수는 다시 생각해보면 결국 변수를 읽으면 이다. 그렇기 때문에 결국 변수도 값이되는 것이다.

  • 그러면 변수는 값이 등장하는 위치정도라고 생각할 수 있다.

    function foo(x){
      return 0;
    }
  • 인자로 x를 넣어주면 이 x도 결국 변수이자 값이 되는 것이다.

  • 리턴할 때도 값이니까 함수를 리턴할 수 있다.

  • 함수에서도 값이 위치할 수 있다는 소리로 foo함수를 호출할 때 값으로써 x로 전달한 값을 함수도 전달할 수 있다는 얘기이다.

    function foo(x) {
    x();
    return function () {};
    }
    const y = foo(function () {});
  • 다시 foo함수가 리턴하는 값은 함수이기 떄문에 다시 함수를 받을 수 있다.

  • 이 인자로 함수를 전달하는 방법을 콜백함수라고 한다.

  • 또한, 값이 함수를 리턴되는 겂을 소프트웨어 공학에서는 보통 함수 합성이라고 한다.

  • 실행되는 순간 만들어내는 테크닉일 때 많이 쓰인다.

  • 함수를 인자로 받고, 함수를 리턴하는 함수, 이런 함수를 일급함수(Higher-Order Function) 라고 한다. 리액트에서는 같은 개념으로 (HOC, Higher-Oder Component)가 있다.

    함수를 이해할 때 함수를 어떤 값을 입력으로 주면 계산해서 출력하는 애라고 이해하지말고, 코드를 묶고 있는 값으로 이해하면 훨씬 쉽다.
    함수를 리턴하는 함수는 어떤 계산 결과를 즉시 확정해서 내보낼 수 없는 것이다.
    완벽하게 확정할 수 없는 것을 확정할 수 있는 코드를 줘서 리턴하는 것과 비슷한 컨셉이다.

📌 JavaScript의 문법은 크게 보면 두가지로 나눌 수 있다.

  • 식: 코드를 실행하면 실행의 결과가 값으로 마무리가 되면 그건 전부다 식이다.

    // ex)
    0, 1+10, foo(), 1+10+foo()
  • 문: 실행했을 때 실행의 결과가 안나온다.

    조건문(conditional statement) : if, switch, (삼항연산자는 식)
    반복문: while, do while, for.. (foreach, map, reduce등은 함수 호출 식)

  • 식과 문의 차이는 세미콜론이 있냐 없냐 차이로 결국 세미콜론은 식의 마무리라 할 수 있다.

🌊 Arrow Function(화살표 함수)

  • return의 생략은 이 함수가 한 줄이라는 것 자체가 바로 return하겠다라고 하는 것을 함유하고 있다.
  • 화살표 함수는 이름이 없기 때문에 재귀호출이 불가능하다.
    const bar = x => x*2;
    console.log(bar(2)); // 4
    //
    const myX = 10;
    const myY = (x) => x * 10;
    console.log(myX, myY(1)); // 10, 10
  • 화살표 함수는 자신의 this가 없다.
  • 화살표 함수는 바로 바깥 범위에서 this를 찾는 것으로 검색을 끝내게 된다.
  • 화살표 함수는 arguments 객체를 바인드 하지 않는다.
  • 화살표 함수는 생성자로서 사용될 수 없으며 new와 함께 사용하면 오류가 발생한다.
    const Foo = () => {};
    const foo = new Foo(); // TypeError: Foo is not a constructor
  • 화살표 함수는 prototype 속성이 없다.
    const Foo = () => {};
    console.log(Foo.prototype); // undefined

📌 new연산자를 호출하면 내부적으로 어떤 메커니즘이 작동하는가?

  • 빈 객체를 만들고 함수한테 전달하는데 이 함수한테 빈 객체가 무엇인지를 알려줄 방법으로 this라고 명명하는 것이다.
  • 그래서 이 함수 안에 thisnew연산자를 호출했을 때 생성된 빈 객체이다. (동적 바인딩)
    function my() {
    //자바스크립트가 생성한 빈객체
    // 동적 바인딩을 이용하는 것
    this.name = 10;
    }
  • new연산자로 호출한 함수는 return이 명시적으로 없어도 반환 값으로 새로 만든 this가 리턴이 된다.
  • 이런 과정이 내부적인 메커니즘은 생성자가 동작된 것이다.
  • new연산자는 이렇게 새로운 객체를 호출할 때마다 만들어 낸다.
  • 함수가 그런 객체를 구조적으로 형태를 만들어 내는 의미에서 생성자라고 부른다. 내부적인 메커니즘으로는 prototype이라는 메커니즘을 사용한다.(다음시간에..)
  • 이렇게 만들어진 객체를 인스턴스 객체라고 한다.
    // 인스턴스 객체
    const g = new my();
    if (g instanceof my) {
      console.log('true'); //  true
    }
    console.log(g); // my {name: 10}
  • 그래서 es6에서 좀 더 명시적으로 나타내기 위해서 class가 등장한다.

🌈 Class

  • Class
    class bar {
      // 생성자
      constructor() {
        this.age = 10;
      }
    }
    console.log(new bar()); // bar {age: 10}
    📌 함수와 클래스가 다른 점
  • constructor가 명확하게 들어나있다.(명시적)
  • new연산자를 앞에다 붙여서 호출을 했을 때 함수new 연산자와 new 연산자 없이 호출이 가능하기 때문에 사용자 입장에서는 new 함수를 감지할 수 없다. (명시적이지 못하다.)
    // 앞에 대문자로 생성자 함수를 구분하는 처절함..
    const Bar(){
      this.age = 10;
    }
    const bar = new Bar();
  • class로 만들면 new호출하지 않으면 호출이 불가능하다.
  • 여기서 this실행 컨텍스트 지시어라고 한다.

🌈 this와 클로저(closure)

🌊 this

  • person 객체
    const person = {
    name: "사승민",
    // arrow는 this가 변하지 않는다.
    getName() {
      // this는 person
      // 실행 컨텍스트
      return this.name;
    }
    };
    console.log(person.getName()); // 사승민
  • personname실행 컨텍스트라고 한다.
  • 실행 컨텍스트는 실행하는 순간 소유자를 자바스크립트 엔진이 실행할 때 확인한다. 그래서 함수 안에서 this를 사용했을 때 호출하는 순간에 소유자와 연결된다.
  • ❗ 여기서 문제는 소유자가 사라지는 순간이 있다.
    const man = person.getName;
    console.log(man()); // 값없음
  • this를 선언하지 않았기 때문에 실행하는 순간 호출자가 확인이 안되서 전역 스코프의 전역 객체에 연결된다는 룰을 가지고 있다.(브라우저의 전역객체는 window 객체)

📌 this를 이렇게 만든 이유

  • this를 이렇게 저렇게 바꾸어가며 활용해라.
  • 🔸 this를 고정하는 방법
  1. 모든 함수가 제공하는 bind()
    const man1 = person.getName.bind(person);
    console.log(man1()); // 사승민
  2. call() : this로 선택한 값과 매개변수로 함수를 호출한다.
  3. apply() : this로 선택한 값과 매개변수로 함수를 호출한다.
    person.getName.call(person);
    // "사승민"
    person.getName.apply(person);
    // "사승민"
  • call()apply()의 동작은 같고, 차이점은 함수에 매개변수를 넘기는 방법으로 apply의 매개변수는 배열이고 call은 문자열을 나열한 것이다.

🌊 클로저(closure)

  • 함수가 호출되면 함수의 스코프가 생성된다.
    function foo(x) {
    // 스코프
    return function(){
      return x;
    }
    }
    const result = foo(10);
    console.log(result()); // 10
  • 이렇게 안쪽 function은 그 스코프 안에 만들어진다.
  • 이때 foo 함수 안에서 접근하는 어떤 변수가 자기 스코프 체인 바깥 쪽 자신을 감싸고 있는 스코프 체인에 있는 변수라면 그걸 함께 들고 있는 영역을 클로저라고 한다.
  • foo함수가 리턴이 될 때 foo함수의 스코프는 같이 사라지는데 받는 인자 x는 없어질 거 같지만 실제 스코프에 있는 x는 없어지고 그걸 따로 저장하고 있는 안쪽 function 함수는 x클로저 영역에 따로 저장하고 있기 때문에 이 때 호출될 때 여전히 x가 존재한다.
  • 이렇게 값을 따로 저장하고 있기 때문에 값을 보호할 용도로 많이 사용한다.

📌 클로저를 사용한 모듈 패턴

const person = {
  age: 10,
}
person.age = 500;
// 클로저이요ㅕㅇ해서 막았따
function makePerson(){
  let age = 10;
  // 모듈패턴
  // 값을 보호해야할때 이렇게 사용했다.
  return {
    getAge(){
      return age;
    },
    setAge(x){
      // 1보다 작고 130보다 크면 age
      age = x > 1 && x < 130 ? x: age;
    }
  }
}
let p = makePerson();
console.log(p.getAge()); // 10
p.setAge(200);
console.log(p.getAge()); // 10 200은 130보다 크니 기본 age
p.setAge(30);
console.log(p.getAge()); // 30

🌈 비동기

  • setTimeout을 사용한 비동기
    setTimeout(function foo(x) {
        console.log("아싸");
      setTimeout(function (y) {
        console.log("우싸");
      }, 2000);
    }, 1000);
  • setTimeout을 사용하여 비동기적으로 실행하였다.
  • 1초가 지난 뒤 아싸가 콘솔에 찍히고, 2초가 지난 뒤 우싸가 찍힌다.
  • 이렇게 여러 단계로 함수를 실행하게 되면 함수에 함수에 함수.. 계속 이렇게 되어 복잡한 구조로 나아간다.
  • 이러한 복잡한 구조를 Callback hell또는 콜백 지옥이라고 한다.

🌊 Promise(프로미스)

  • 위와 같이 콜백 지옥을 해결하기 위해 나온 것이 Promise이다.
    // promise1
    const p1 = new Promise((resolve, reject) => {
        // 이것 또한 클로저
      setTimeout(() => {
        // 이 안에서만 호출
        resolve("응답1");
      }, 1000);
    });
    // promise2
    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("응답2");
      }, 1000);
    });
    // 메소드 체이닝
    p1.then(
      p2.then((r) => console.log(r)))
      .catch(function(){});
    // 응답2
    // Promise {<fulfilled>: "응답1"}
  • new Promise는 인스턴스 객체를 리턴하는데 이 인스턴스 객체 안에는 then이라고 하는 속성(메소드) 함수가 들어가 있다.
  • 이 비동기 작업이 성공하면 then을 사용하며 then은 resolve를 호출한다.
  • reject함수를 호출하면 catch안에 기술된 함수를 호출해준다.

🌊 비동기 함수(Async/await)

  • 비동기 함수의 background는 결국 Promise이다.
  • 비동기함수를 사용하면 동기적인 코드로 작성할 수 있다.
    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    async function main(){
    console.log('1');
    try {
      const x = await delay(2000);
    } catch (error) {
      //reject
      console.log(error);
    }
    console.log('2');
    }
    main();
  • 1이 찍히고 delay 함수가 실행되고 2초 뒤 2가 실행된다.
  • await연산자는 Promise 객체 즉, thenable 객체가 있으면 안에서 resolve가 호출됬을 때 resolve 함수가 반환하는 값을 마치 리턴 값으로 넘겨주는 식으로 된다.
  • 여기서 javascript에서 많이 사용하지 않던 try/catch문을 사용하여 reject가 에러로 잡히게 된다.

📌 결국 콜백의 깊은 depth를 일자로 펼치기 위해서 Promise가 만들어졌고 그 Promise의 backgroud 위에 진짜 일자 동기 코드를 쓰기 위해서 async 함수가 만들어졌다.

🌊 제너레이터

  • redux-saga할 때 진행..

📌 커링이란?

  • 인자가 여러 개 있을 때 인자 하나하나가 함수 하나씩 하나씩 배분해서 쪼개는 것을 커링이라고 부른다.
  • 클로저는 항상 커링이 된다.
  • 클로저는 커링을 위한 기술중 하나이다.
    function foo(a){
    return function(b){
      return function(c){
        return a + b + c;
      }
    }
    }

🚀 Redux 만들어보기

🌈 리덕스의 store 생성

  • 중앙에 스토어 하나를 생성하는 개념이다.(객체이다.)
  • 객체를 만드는 함수를 제공하는데 그것이 리덕스에서 createStore이다.
    // redux.js
    export const createStore = () => {
      const state = {};
      return state;
    }
  • 리덕스의 컨셉은 밑으로 흘러가면서 데이터를 절대 컴포넌트가 직접적으로 변경 불가하다. 즉, immutable하고 컴포넌트는 데이터를 받아서 그리기만 해준다.
  • 그렇기 때문에 클로저로 상태를 숨겨주어야 한다.
    export const createStore = () => {
      const state = {};
      // 새로운 객체를 내보낸다.
        const getState = () => ({ ...state });
      return {getState};
    }
  • 바깥에서 상태를 알고싶으면 store객체의 getState를 호출해야만 볼 수 있다.

🌈 상태를 변경하기

  • 그 상태를 바꾸는 코드는 리덕스한테 상태를 바꾸는 함수로 전달해준다.
  • 이것을 리듀서라고 부른다.
    // index.js
    function reducer(state = {}, action) {
    if (action.type === "increment") {
      return {
        ...state,
        count: state.count ? state.count + 1 : 1
      };
      //state.abc = 'OK';
    } else if (action.type === "reset") {
      return {
        ...state,
        count: action.resetCount
      };
    }
    return state;
    }
  • createStore한테 리듀서 함수를 전달해주어야 한다.
  • 그러기 위해서는 약속이 필요한데 리듀서 함수의 상태가 변경될 때 이 리듀서 함수를 호출하면 현재 상태를 넘겨준다.
  • 무엇을 바꾸는지 모르기 때문에 간단하게 이 문제를 해결할 수 있는데 두번째 인자에는 객체 하나를 풀고, 이 객체는 type이라는 문자열을 갖고있는 속성(action)을 현재 state 상태로 업데이트 시켜준다.(새로운 객체를 반환)
    // createStore 한테 리듀서 함수 전달.
    // redux.js
    export function createStore(reducer) {
      let state;
      const getState = () => ({ ...state });
      const dispatch = (action) => {
        state = reducer(state, action);
      };
      return {
        getState,
        dispatch
      };
    }
    // index.js
    import { createStore } from "./redux";
    function reducer(state = {}, action) {
      if (action.type === "increment") {
          return {
            ...state,
            count: state.count ? state.count + 1 : 1
          };
            //state.abc = 'OK';
      } else if (action.type === "reset") {
          return {
            ...state,
            count: action.resetCount
          };
      }
      return state;
    }
    const store = createStore(reducer);
    function actionCreator(type, data) {
      return {
        ...data,
        type: type
      };
    }
    store.dispatch(actionCreator("increment")); // Object {count: 1}
    store.dispatch(actionCreator("increment")); // Object {count: 2}
    store.dispatch(actionCreator("increment")); // Object {count: 3}
    store.dispatch(actionCreator("increment")); // Object {count: 4}
    store.dispatch(actionCreator("increment")); // Object {count: 5}
    store.dispatch(actionCreator("reset"));     // Object {count: undefined}
    store.dispatch(actionCreator("increment")); // Object {count: 1}
  • action 문자열을 상수처리와 store.dispatch(actionCreator("increment"));이렇게 계속 쓸 수는 없다. increment함수와 reset함수를 생성해준다.
    // index.js
    import { createStore } from "./redux";
    const INCREMENT = "increment";
    const RESET = "reset";
    function reducer(state = {}, action) {
      if (action.type === INCREMENT) {
          return {
            ...state,
            count: state.count ? state.count + 1 : 1
          };
            //state.abc = 'OK';
      } else if (action.type === RESET) {
          return {
            ...state,
            count: action.resetCount
          };
      }
      return state;
    }
    const store = createStore(reducer);
    function actionCreator(type, data) {
      return {
        ...data,
        type: type
      };
    }
    // 증가
    function increment() {
        store.dispatch(actionCreator(INCREMENT));
    }
    // 리셋
    function reset() {
        store.dispatch(actionCreator(RESET, { resetCount: 0 }));
    }
    increment(); // Object {count: 1}
    increment(); // Object {count: 2}
    increment(); // Object {count: 3}
    increment(); // Object {count: 4}
    increment(); // Object {count: 5}
    reset();     // Object {count: 0}
    increment(); // Object {count: 1}

🌈 변경된 상태를 확인하기

📌 스토어에 바뀐 상태를 어떻게 알까?

  • 직접 getState를 호출해서 알 수 있다.

  • 하지만 UI에서 언제 store가 바뀔지 알아서 코드로 적시할 수 없다.

  • 리듀서에서 리덕스가 dispatch가 불러서 리듀서 함수를 호출해서 UI가 쓴 코드로 state를 바꾼 것을 방금 내부의 클로저에 잡혀있는 내 store에다가 변경됬다는 것을 바깥 쪽으로 통지를 해주어야 한다. 즉, 함수를 호출해서 사용한다.

  • 통지함수 update함수 생성

  • subscribe 함수는 인자를 하나 받아서 listeners배열에 저장한다.

  • listenerssubscribe함수에서 받은 function을 넣는다.

    // redux.js
    export function createStore(reducer) {
      let state;
      const listeners = [];
      const getState = () => ({ ...state });
    
      const dispatch = (action) => {
        state = reducer(state, action);
        listeners.forEach((fn) => fn());
      };
    
      const subscribe = (fn) => {
        listeners.push(fn);
      };
    
      return {
        getState,
        dispatch,
        subscribe
      };
    }
    //index.js
    // 생략..
    const store = createStore(reducer);
    function update() {
        console.log(store.getState());
    }
    store.subscribe(update);
    // 생략..

📌 리덕스 만들기 전체 소스는 GitHub 참조 ✌

🚀 2회차 후기

이번 시간은 정말 많은 것을 배운 시간이였다!! ✨
리액트를 사용하고 공부를 하였지만, 직접 리덕스를 만들어보고 그 안에 어떤식으로 흘러가고 어떤 기술이 쓰였는지에 대해서 한번도 생각해보지 못했던 내 자신한테 부끄러워지만 이번 강의로 확실히 알고가서 너무 좋았다. 😀🚀
앞으로 표면적으로 보이는 것만 사용하지 않고, 한 번더 생각하고 이건 왜 이렇게 되는 걸까? 라는 고민을 해보도록 노력해야겠라는 생각을 많이 했다. 😥
또한, 자바스크립트의 단순하고 당연히 이정도는 알고있지고 생각했던 건 오산이였다..
자바스크립트의 기본적인 개념이더라도 이런 과정이 있어서 이렇게 생겼났다 라는 것을 명확하게 설명해주셔서 간단하다고 생각했던 개념들도 다른관점에서 볼 수 있던 계기가 되었다. 😤
앞으로의 강의가 더욱 더 기대가 되고 이번에 운 좋게 강의를 들어서 너무 좋은 기회를 잡았다고 생각이 들었다!! 🎊🎉