0%

Comments

  1. 코드의 단위적 로직이 복잡한 것들 (Business logic complexity) 만 주석 처리를 하자!

    • 좋은 코드는 그 자체로 이해하기 쉬우므로, 주석은 복잡한 코드에 대한 사과와 같다.

    Business Logic Complexity

    Many organizations adopt software to support their business process and business logic is embedded in their system.

    To solve this problem we define business logic complexity to capture business logic from the source code and to quantify how hard it is to understand.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // bad
    function hashIt(data) {
    // The hash
    let hash = 0;

    // Length of string
    const length = data.length;

    // Loop through every character in data
    for (let i = 0; i < length; i++) {
    // Get character code.
    const char = data.charCodeAt(i);
    // Make the hash
    hash = (hash << 5) - hash + char;
    // Convert to 32-bit integer
    hash &= hash;
    }
    }

    // good
    function hashIt(data) {
    let hash = 0;
    const length = data.length;

    for (let i = 0; i < length; i++) {
    const char = data.charCodeAt(i);
    hash = (hash << 5) - hash + char;

    // Convert to 32-bit integer
    hash &= hash;
    }
    }
  2. 주석 처리된 코드를 codebase에 남겨놓지 말고 지워버리기!

    • 형상 관리 툴 (버전 관리; version control) 이 이런 코드들을 남겨놓기 위함이니, 오래된 코드는 옛 버전에 남겨놓자.
    1
    2
    3
    4
    5
    6
    7
    8
    // bad
    doStuff();
    // doOtherStuff();
    // doSomeMoreStuff();
    // doSoMuchStuff();

    // good
    doStuff();
  3. 기록용 주석은 사용하지 말자

    • 위와 동일하게, 형상 관리 툴 (git : git log 를 통해 이전 버전들 확인) 을 이용하고 필요없는 주석은 모두 날려버리자.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // bad
    /**
    * 2016-12-20: Removed monads, didn't understand them (RM)
    * 2016-10-01: Improved using special monads (JP)
    * 2016-02-03: Removed type-checking (LI)
    * 2015-03-14: Added combine with type-checking (JR)
    */
    function combine(a, b) {
    return a + b;
    }

    // good
    function combine(a, b) {
    return a + b;
    }
  4. 위치 표시 주석도 피하자!

    • 적절한 indentation, 함수명변수명 , 포맷팅 을 이용한다면 기능 구분을 위한 정신없는 positional marker comments 는 필요하지 않다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // bad
    ////////////////////////////////////////////////////////////////////////////////
    // Scope Model Instantiation
    ////////////////////////////////////////////////////////////////////////////////
    $scope.model = {
    menu: "foo",
    nav: "bar"
    };

    ////////////////////////////////////////////////////////////////////////////////
    // Action setup
    ////////////////////////////////////////////////////////////////////////////////
    const actions = function() {
    // ...
    };

    // good
    $scope.model = {
    menu: "foo",
    nav: "bar"
    };

    const actions = function() {
    // ...
    };

Formatting

포맷팅은 주관적이다.

포맷팅 체크를 해주는 자동화 도구 가 많이 있기 때문에, 공통적으로 하나를 골라 쓰는 것이 합리적이다. 자동으로 교정되는 서식 외의 것들에 대해서는 다음과 같은 지침을 따르는 것이 좋으나, 무엇보다 일관성 있게 적용하는 것이 중요하다.


  1. 일관된 대소문자를 사용할 것

    • 특히 JS에서는 타입을 명시하지 않기 때문에 변수와 함수의 대소문자를 통해 특성을 파악할 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const DAYS_IN_WEEK = 7;
    // const daysInMonth = 30;
    const DAYS_IN_MONTH = 30;

    /*
    const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
    const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];
    */
    const SONGS = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
    const ARTISTS = ["ACDC", "Led Zeppelin", "The Beatles"];

    function eraseDatabase() {}
    // function restore_database() {}
    function restoreDatabase() {}

    // class animal {}
    class Animal {}
    class Alpaca {}
  2. 함수 callers와 함수 callees는 가깝게 위치시키자

    • 우리는 코드를 위에서부터 아래로 순차적으로 읽어나가기 때문에, caller와 callee를 수직적으로 가깝게 위치시키는 것이 좋다.
    • 즉, caller 밑에 callee를 놓자.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    // bad
    class PerformanceReview {
    constructor(employee) {
    this.employee = employee;
    }

    lookupPeers() {
    return db.lookup(this.employee, "peers");
    }

    lookupManager() {
    return db.lookup(this.employee, "manager");
    }

    getPeerReviews() {
    const peers = this.lookupPeers();
    // ...
    }

    perfReview() {
    this.getPeerReviews();
    this.getManagerReview();
    this.getSelfReview();
    }

    getManagerReview() {
    const manager = this.lookupManager();
    }

    getSelfReview() {
    // ...
    }
    }

    const review = new PerformanceReview(employee);
    review.perfReview();

    // good
    class PerformanceReview {
    constructor(employee) {
    this.employee = employee;
    }

    perfReview() {
    this.getPeerReviews(); // 1
    this.getManagerReview(); // 2
    this.getSelfReview(); // 3
    }

    // 1
    getPeerReviews() {
    const peers = this.lookupPeers(); // 1-1
    // ...
    }

    // 1-1
    lookupPeers() {
    return db.lookup(this.employee, "peers");
    }

    // 2
    getManagerReview() {
    const manager = this.lookupManager(); // 2-1
    }

    // 2-1
    lookupManager() {
    return db.lookup(this.employee, "manager");
    }

    // 3
    getSelfReview() {
    // ...
    }
    }

    const review = new PerformanceReview(employee);
    review.perfReview();

Error Handling


에러가 런타임에서 성공적으로 확인된 경우,

  1. 현재 스택에서 함수 실행을 중단하고
  2. 프로세스 노드에서 프로세스를 종료하고
  3. 스택을 추적해 콘솔에서 사용자에게 에러에 대한 설명을 제공한다.

  1. 단순히 에러를 확인만 하지 말고, Handling 하자!

    • console.log 를 사용하면 에러 로그와 헷갈리기 쉽기 때문에 자제하고,
    • try - catch 문으로 감쌌다면 에러를 예상한 것이므로, 해당 에러에 대한 해결 방안을 마련해 놓아야 한다.

    👇

    • console.error 또는 console.exception 활용하기

      error

      1
      2
      3
      4
      console.debug('디버그 메세지'); 
      console.info('정보 메세지');
      console.warn('경고 메세지');
      console.error('에러 입니다.');
    • 유저에게 error를 알리기

    • 서비스 자체에 error를 기록하기

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // bad
    try {
    functionThatMightThrow();
    } catch (error) {
    // just logging out
    console.log(error);
    }

    // good
    // error "handling"
    try {
    functionThatMightThrow();
    } catch (error) {
    // 1. console.error
    // === console.exception
    console.error(error);
    // 2. 유저에게 알리기
    notifyUserOfError(error);
    // 3. 서비스 자체에 에러 기록하기
    reportErrorToService(error);
    // ... etc
    }
  2. Promise 가 반환한 rejected 를 넘기지 말고, Handling 하자!

    Handling 방법은 위와 동일하게, 각 에러에 맞게 정의한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // bad
    getdata()
    .then(data => {
    functionThatMightThrow(data);
    })
    .catch(error => {
    // just logging out...
    console.log(error);
    });

    // good
    getdata()
    .then(data => {
    functionThatMightThrow(data);
    })
    .catch(error => {
    // 1.
    console.error(error);
    // 2.
    notifyUserOfError(error);
    // 3.
    reportErrorToService(error);
    // ...etc
    });

기본적인 Handling 방법

JAVA 기준


  • 오류 코드보다 예외 를 사용할 것

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void sendShutDown() {
    try {
    tryToShutDown();
    } catch (DeviceShutDownError e) {
    logger.log(e);
    }
    }

    private void tryToShutDown() throws DeviceShutDownError {
    DeviceHandle handle = getHandle(DEV1);
    DeviceRecord record = retrieveDeviceRecord(handle);
    pauseDevice(handle);
    clearDeviceWorkQueue(handle);
    closeDevice(handle);
    }
  • try-catch-finally 부터 작성할 것

    • try 문은 transaction처럼 동작하는 실행코드로, catch 문은 try문에 관계없이 프로그램을 일관적인 상태로 유지하도록 한다.
  • 예외에 의미를 제공할 것

    • 예외를 던질 때는 전후 상황을 충분히 덧붙인다.
    • 실패한 연산 이름과 실패 유형도 언급한다.
  • 호출자를 고려해 예외 클래스를 정의할 것

    • 써드파티 라이브러리를 사용하는 경우 그것들을 wrapping함으로써 라이브러리 교체 등의 변경이 있는 경우 대응하기 쉬워진다.
    • Wrapper 클래스를 이용하여 호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하므로 의존성이 크게 감소한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    try {
    port.open();
    } catch (PortDeviceFailure e) {
    reportError(e);
    logger.log(e.getMessage(), e);
    } finally {
    ...
    }

    public class LocalPort {
    ...

    public void open() {
    try {
    innerPort.open();
    } catch (DeviceResponseException e) {
    throw new PortDeviceFailure(e);
    } catch (ATM1212UnlockedException e) {
    throw new PortDeviceFailure(e);
    } catch (GMXError e) {
    throw new PortDeviceFailure(e);
    }
    }
    }
  • 에러 시 null 을 반환하거나 전달하지 말 것



Ref

https://ibrahimovic.tistory.com/39

https://nesoy.github.io/articles/2018-02/CleanCode-ErrorHandle

React 실전


조건부 렌더링

if-else : 조건에 따라 다른 컴포넌트를 렌더링할 때 사용한다.

  1. && : 조건이 true 면 다음 컴포넌트 (설정값) 를 보여줌 / 다음 조건으로 넘어감
  2. 삼항연산자 : 조건이 true 면 첫번째, 조건이 false 면 두번째 설정값 보여줌
  3. switch : 조건이 여러 개 일 때 사용하면 좋음

List & Key

  • list를 렌더링 할 때는 반드시 props에 key를 설정해야 한다.
  • key를 설정하지 않으면 오류 메시지가 뜬다.
  • key 는 각 요소의 고유값 (id, index 등) 으로 설정해야 한다.
  • key가 있어야 배열을 효율적으로 렌더링 할 수 있다.
    • 배열이 변경되면, 바뀌는 부분만 업데이트 한다.
    • key를 설정하지 않으면, 하나만 바뀌어도 전체가 리렌더링

BEM 방법론

Block Element Modifier


1
.block__element--modifier
  • css className을 정하는 방법론 중 하나
    • class 이름 에만 사용 가능함
    • camelCase는 class 이름에는 사용하지 않음
      • 대신 camel-case 처럼 하이픈 사용!
  • 구조를 쉽게 파악할 수 있다는 장점이 있음

  1. block : 재사용 가능한, 기능적으로 독립적인 컴포넌트
  2. element : block을 구성 하는 단위
  3. modifier : block 이나 element 의 속성

장점 단점
className의 중복 방지 className이 길고 복잡해질 수 있음
className 만으로 마크업 구조 파악 가능
SASS에서 부모 선택자와 사용하면 편리함
유지 보수에 유리함

SASS

Syntactically Awesome Style Sheets


  • CSS Pre-processor (전처리기)

    • SASS 문법으로 작성하나 CSS로 컴파일 됨
  • CSS의 불편한 점들을 보완하고, 확장된 기능을 사용할 수 있다.

    • CSS의 확장판으로 볼 수 있으나, 문법이 다르다.

SCSS

Sassy CCS


  • SASS의 기능을 지원하는 CSS의 Superset

    • CSS의 확장판
  • 기존의 CSS 문법과 유사하고, SASS보다 사용성이 높다.

  • 변수, 연산자, 내장함수, 중첩 등 다양한 기능 지원

    • 변수

      • $ 로 변수 선언
      • block scope
      • 재할당 가능
    • 연산

      • 연산자 사용 가능 (e.g. +, -, *, /, %, ==, !=)
    • import

      • @import "다른 스타일 파일"
      • scss 확장자 생략 가능
      • _ 로 시작하는 파일은 컴파일되지 않음
    • extend

      • @extend 로 상속
    • mixin

      • @mixin 으로 인수를 받을 수 있음
      • @include 를 이용해 선언한 mixin 사용
    • 중첩

      • 선택자 중첩 가능
      • & 로 부모 선택자 사용

Grid Layout

2차원 레이아웃 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Grid Layout 적용
display: grid;

# 가로/ 세로 기준 나열 방법
grid-template-columns: 1fr 2fr 3fr;
grid-template-rows: 100px 200px;

# 속성값으로 사용할 수 있는 함수
repeat(반복횟수, 반복값)
minmax(최소값, 최대값)
repeat(auto-fit, minmax(20%, auto))

# Grid 셀을 병합할 때

grid-column-start: 2;
grid-column-end: 4;
### grid-column: 2 / 4;

grid-row-start: 1;
grid-row-end: 3;
### grid-row: 1 / 3;

  • row 또는 column 만을 사용한 1차원 (i.e. Flexbox) 을 넘어 2차원 시스템 (i.e. Grid) 으로 유연하게 레이아웃 배치 및 사용 가능
  • 왼쪽 위부터 차례대로 1번~ 순서 할당
  • 보통 repeat의 반복값에 minmax 함수 이용
  • auto-fill : 간격이 자동적으로 조절됨, 균등하게 나누어짐
  • auto-fit : 정해진 구역에서 자리 할당
  • grid-@@-end : 끝나는 자리 +1
    • e.g. grid-column-end = 3 일 때, end 위치는 2

React Router

화면 전환을 도와주는 React의 서드파티 라이브러리


  • SPA에서는 하나의 html 파일로 변화하는 부분만 교체함

  • SPA에서는 React Router의 Link 를 이용해서 페이지 이동

    • a tag 사용하면 안 됨 ( 기존의 웹 페이지는 <a href=""></a> 로 페이지 이동 )

  1. Link

    • 페이지 이동

    • 브라우저의 주소만 변경되고 레이아웃에서 변경된 부분만 리렌더링 됨, 전체 화면은 동일 (Single Page Application) 하다!

  2. Route

    • 주소가 path 일 때, component를 렌더링한다.
    • exact : path가 정확하게 일치할 때만 이동
  3. Router

    • Link, Route 태그를 묶는 인터페이스
      • <BrowserRouter> 태그 안에 <Route> 태그가 존재해야 함!
    • 최상위에 하나만 존재한다.
    • BrowserRouter >> HashRouter
      • BrowserRouter : history 데이터 (API) 가 내장됨
  4. Switch

    • 첫번째로 매칭되는 path의 component를 렌더링한다.
    • Default 처리를 할 때 유용하다. (e.g. “Page Not Found”)
    • Switch를 사용하지 않은 상태에서 path가 없는 컴포넌트가 밑에 있을 경우 순서에 상관 없이 해당 컴포넌트부터 렌더링 된다. (순서가 무너지게 됨)
  5. withRouter

    • 고차 컴포넌트 ( Higher Order Component )
    • Route가 아닌 컴포넌트에서 match, history, location 사용 가능
    1
    2
    3
    4
    5
    import { withRouter } from 'react-router-dom';

    const ComponentName = ({ history }) => {};

    export default withRouter(ComponentName);

Route의 기본 props

  1. history
    • 주소를 임의로 변경하거나 돌아갈 수 있음
    • history.push() : 새로운 경로를 history stack에 push하여 이동
    • history stack에 방문 기록을 저장하기 때문에 go / goBack 등이 가능함
  2. match
    • Route path와 URL 매칭에 대한 정보
    • match.params : path 설정값
      • 현재 주소창의 위치를 반환
  3. location
    • 현재 페이지의 정보
    • location.search : url 쿼리스트링
    • history의 속성에도 location이 있음

React Hooks

  • useRef

    React에서 DOM 요소를 가져올 때 사용함

    • const 변수명 = useRef(); 로 선언한다.

    • 가져올 요소의 attribute에 ref={변수명} 을 추가한다.

    • 변수명.current 로 DOM 요소를 가져올 수 있다.


json-server

JSON 파일로 로컬에 연습용 서버 만들기


  • src 폴더 밖에 data.json 파일을 생성한 후, 더미 데이터 넣기
  • React 기본 port가 3000번이기 때문에, 서버는 다르게 지정할 것!

Testing

Testing 은 Shipping (배포) 보다 중요하다.

만약 테스트가 없거나 적절하지 않은 양만큼 (부족한) 만 존재할 경우, 배포할 때마다 코드를 break 했는지 혹은 수정한 코드가 올바르게 작동할지 확신할 수 없다.

적절한 양을 정하는 요소는 팀과 상황에 따라 다르지만, 100%의 Coverage ( 보호 범위 , 즉, 모든 statement와 branch가 보호됨) 를 갖는 것은 개발 시에 안전함과 자신감을 제공한다.

따라서, testing framework 에 더하여 good coverage tool 또한 필요 하다.

e.g.

Testing Framework : Mocha · QUnit · Jasmine · Karma · Intern · AVA, etc.

Coverage Tool : istanbul · Cobertura · CodeCover · Coverage.py · EMMA, etc.


Test Driven Development (TDD)

  • 모든 feature / module 에 대한 테스트를 정의하자.
  • 목표 coverage 를 도달한 상태에서 새로운 feature을 배포하자.
    • 도달하지 못했다면, refactoring 부터 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import assert from "assert";

// bad
describe("MomentJS", () => {
// date boundary를 ☹️한 데 모아😡 처리함.
it("handles date boundaries", () => {
let date;

date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);

date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);

date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});

// good
describe("MomentJS", () => {
// date boundary handler를 😄여러 조건으로 나누어🤩 정의함.

// 30일을 가진 month 의 경우
it("handles 30-day months", () => {
const date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
});

// 윤년의 경우
it("handles leap year", () => {
const date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
});

// 그 외, 31일을 가진 month && 윤년이 아닌 경우
it("handles non-leap year", () => {
const date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});

SOLID

  1. Single Responsibility Principle (SRP)

    단일 책임 원칙

    • “클래스가 수정되어야 하는 이유가 2개 이상으로 존재하면 안 된다.”
    • 하나의 클래스에는 개념적으로 응집된 기능만을 포함하는 것이 좋다.
    • 하나의 클래스에 여러 기능이 포함되어 있는 경우 작은 변화가 이와 연결된 다른 모듈들에 영향을 줄 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // bad
    class UserSettings {
    constructor(user) {
    this.user = user;
    }

    changeSettings(settings) {
    if (this.verifyCredentials()) {
    // ...
    }
    }

    verifyCredentials() {
    // ...
    }
    }

    // good
    // Authorization은 따로 클래스 정의
    class UserAuth {
    constructor(user) {
    this.user = user;
    }

    verifyCredentials() {
    // ...
    }
    }

    // Setting과 Authorization 분리
    class UserSettings {
    constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
    }

    changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
    // ...
    }
    }
    }
  1. Open / Closed Principle (OCP)

    개방 / 폐쇄 원칙

    • “소프트웨어 개체 (e.g. Class, Module, Function, etc.) 는 확장이 용이하지만 변경은 어려워야 한다.”
    • 즉, 사용자가 이미 존재하는 코드를 (직접적으로) 수정하지 않아도 새로운 기능을 추가할 수 있도록 설정해야 한다.
    1. 확장 에 대해 열려 있다.
      • 모듈의 동작 (기능) 을 확장할 수 있음
      • 요구 사항이 변경될 때, 이에 맞게 새로운 동작을 추가해 모듈을 확장함
      • 모듈이 하는 일을 변경하는 것
    2. 수정 에 대해 닫혀 있다.
      • 모듈의 소스 / 바이너리 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있어야 함

앞서 설명했 듯,

많은 모듈 (클래스) 중 하나를 수정할 때 이에 의존하는 다른 모듈들 또한 수정해야 하는 코드는 좋지 않은 코드를 의미한다.

  • 시스템의 구조를 올바르게 리팩토링 하여 앞선 변경과 같은 유형의 수정 사항이 발생할 경우 더 이상의 수정을 요구하지 않고, 잘 동작하고 있던 기존 코드의 변경 없이 새로운 코드를 추가 하여 기능의 추가와 변경이 가능하도록 한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    class AjaxAdapter extends Adapter {
    constructor() {
    super();
    this.name = "ajaxAdapter";
    }

    // good
    // 즉, REFACTORING 을 잘한 것이 OCP를 적용한 것
    request(url) {
    // request and return promise
    }
    }

    class NodeAdapter extends Adapter {
    constructor() {
    super();
    this.name = "nodeAdapter";
    }

    // good
    request(url) {
    // request and return promise
    }
    }

    class HttpRequester {
    constructor(adapter) {
    this.adapter = adapter;
    }

    fetch(url) {
    // bad
    if (this.adapter.name === "ajaxAdapter") {
    return makeAjaxCall(url).then(response => {
    // transform response and return
    });
    } else if (this.adapter.name === "nodeAdapter") {
    return makeHttpCall(url).then(response => {
    // transform response and return
    });
    }

    // good
    return this.adapter.request(url).then(response => {});

    }
    }

    function makeAjaxCall(url) {
    // request and return promise
    }

    function makeHttpCall(url) {
    // request and return promise
    }
  1. Liskov Substitution Principle (LSP)

    리스코프 치환 원칙

    • “만약 S가 T의 하위유형이라면, T 객체는 (바람직한) 속성 (e.g. 정확도, 수행성 등) 을 변경하지 않아도 S 객체로 변경 가능해야 한다.”

    • 즉, 부모 클래스와 자식 클래스가 있을 때, 이들을 서로 바꾸어 적용해도 올바른 결과가 나와야 한다.

      • 정사각형 - 직사각형 클래스에서, 정사각형은 직사각형이지만, 이들을 is-a 관계로 정의한다면 문제가 발생한다.

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        99
        100
        101
        // bad
        class Rectangle {
        constructor() {
        this.width = 0;
        this.height = 0;
        }

        setColor(color) {
        // ...
        }

        render(area) {
        // ...
        }

        setWidth(width) {
        this.width = width;
        }

        setHeight(height) {
        this.height = height;
        }

        getArea() {
        return this.width * this.height;
        }
        }

        class Square extends Rectangle {
        setWidth(width) {
        this.width = width;
        this.height = width;
        }

        setHeight(height) {
        this.width = height;
        this.height = height;
        }
        }

        function renderLargeRectangles(rectangles) {
        rectangles.forEach(rectangle => {
        rectangle.setWidth(4);
        rectangle.setHeight(5);
        const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
        rectangle.render(area);
        });
        }

        const rectangles = [new Rectangle(), new Rectangle(), new Square()];
        renderLargeRectangles(rectangles);

        // good
        // 1. Shape 관련 메소드는 분리하여 따로 클래스 정의
        class Shape {
        setColor(color) {
        // ...
        }

        render(area) {
        // ...
        }
        }

        // 2. 직사각형은 Shape 를 상속받는다.
        class Rectangle extends Shape {
        constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
        }

        getArea() {
        return this.width * this.height;
        }
        }

        // 3. 정사각형도 Shape 를 상속받지만, 직사각형과 다른 특징을 가지므로 직사각형과 'is-a' 관계로 정의 (=== 상속) 하지 않는다.
        class Square extends Shape {
        constructor(length) {
        super();
        this.length = length;
        }

        // 차이점
        getArea() {
        return this.length * this.length;
        }
        }

        // 4. Shape 를 통해 속성값을 설정해놓아 이를 분리하여 동작을 할 수 있도록 한다. 이로써 여러 모듈 (클래스) 가 엉키지 않고 독립적으로 작용하여 에러를 줄일 수 있으며, 다른 속성을 가진 객체들을 유연하게 다룰 수 있다.
        function renderLargeShapes(shapes) {
        shapes.forEach(shape => {
        const area = shape.getArea();
        shape.render(area);
        });
        }

        // 속성값 constructor의 인자로 전달
        const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
        renderLargeShapes(shapes);
  1. Interface Segregation Principle (ISP)

    인터페이스 분리 원칙

    자바스크립트는 인터페이스 (i.e. type) 가 존재하지 않기 때문에 해당 원칙이 명확하게 적용되지는 않지만, type 시스템이 존재하지 않음에도 불구하고 중요하게 작용하는 원칙이다.

    • “클라이언트는 그가 사용하지 않는 인터페이스에 의존하도록 강요받지 않아야 한다.”
    • 자바스크립트는 duck typing 이므로 여기서 인터페이스는 암시적인 요소로 존재한다.
      • e.g. 큰 setting 객체들을 요구하는 클래스를 예시로 들면,
      • 클라이언트는 대게 모든 setting을 필요로 하지는 않기 때문에, 많은 양의 option을 설정하도록 요구하는 것은 좋지 않다.
      • 따라서, Setting을 optional하게 만드는 것은 “fat interface” 를 방지한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    // bad
    class DOMTraverser {
    constructor(settings) {
    this.settings = settings;
    this.setup();
    }

    setup() {
    this.rootNode = this.settings.rootNode;
    this.settings.animationModule.setup();
    }

    traverse() {
    // ...
    }
    }

    const $ = new DOMTraverser({
    rootNode: document.getElementsByTagName("body"),
    animationModule() {} // Most of the time, we won't need to animate when traversing.
    // ...
    });

    // good
    class DOMTraverser {
    constructor(settings) {
    this.settings = settings;
    // options 속성을 추가한다.
    this.options = settings.options;
    this.setup();
    }

    setup() {
    this.rootNode = this.settings.rootNode;
    // settings에 직접 접근하지 않고 setupOptions() 메소드를 이용한다.
    this.setupOptions();
    }

    // options를 세팅하는 메소드를 따로 작성한다.
    setupOptions() {
    // 이로써 options에 접근하도록 한다.
    // setter과 같은 역할
    if (this.options.animationModule) {
    // ...
    }
    }

    traverse() {
    // ...
    }
    }

    const $ = new DOMTraverser({
    rootNode: document.getElementsByTagName("body"),
    // 'Most of the time, we won't need to animate when traversing.' 이므로,
    // 이를 options로 설정하여 무조건 실행되지 않고 선택적으로 실행될 수 있도록 한다.
    options: {
    animationModule() {}
    }
    });
  1. Dependency Inversion Principle (DIP)

    의존성 역전 원칙

    1. 상위 모듈은 하위 모듈에 종속되어서는 안 된다. 둘 다 추상화에 의존해야 한다.
    2. 추상화는 세부사항에 의존하면 안 된다. 세부사항이 추상화에 의존해야 한다.
    • 의존성 ( Coupling ) 이 높을수록 코드를 리팩토링하기 어렵다. 👉 나쁜 개발 습관
    • 상위 모듈이 하위 모듈의 세부사항을 알지 못하므로, 의존성을 감소시킬 수 있다.
    • 자바스크립트는 인터페이스가 없어 추상화에 의존한다는 것은 암시적인 것 (약속) 이므로, 다른 객체나 클래스에 노출되는 메소드와 속성 자체가 이에 (암시적인 약속, 추상화) 해당한다고 볼 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    class InventoryRequester {
    constructor() {
    this.REQ_METHODS = ["HTTP"];
    }

    // 암시적인 약속
    // InventoryTracker에 대한 모든 요청 모듈은 requestItems 메소드를 가질 것이다.
    requestItem(item) {
    // ...
    }
    }
    /*
    // Requester를 버전 별로 (e.g. V1, V2) 나누어서 클래스로 생성 가능하다.
    class InventoryRequesterV1 {
    constructor() {
    this.REQ_METHODS = ["HTTP"];
    }

    requestItem(item) {
    // ...
    }
    }

    class InventoryRequesterV2 {
    constructor() {
    this.REQ_METHODS = ["WS"];
    }

    requestItem(item) {
    // ...
    }
    }
    */


    // bad
    class InventoryTracker {
    constructor(items) {
    this.items = items;

    // BAD: We have created a dependency on a 'specific' request implementation.
    // We should just have requestItems 'depend on a request method': `request`
    this.requester = new InventoryRequester();
    }

    requestItems() {
    this.items.forEach(item => {
    this.requester.requestItem(item);
    });
    }
    }

    const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
    inventoryTracker.requestItems();


    // good
    class InventoryTracker {
    constructor(items, requester) {
    this.items = items;
    this.requester = requester;
    }

    requestItems() {
    this.items.forEach(item => {
    this.requester.requestItem(item);
    });
    }
    }

    // By constructing our dependencies externally and injecting them, we can easily
    // substitute our request module for a fancy new one that uses WebSockets.
    const inventoryTracker = new InventoryTracker(
    ["apples", "bananas"],
    // requester를 인자로 전달하여 tracker의 default requester로 객체를 설정하(여 코드를 정의하)는 것이 아니라 tracker 생성 시 개별적으로 적용 (생성) 할 수 있게 설정한다.
    new InventoryRequesterV2()
    );
    inventoryTracker.requestItems();

Classes

  1. ES5의 함수보다 ES6(i.e. ES2015)의 클래스를 사용하기

    • 상속이 필요한 경우에는 ES6 클래스를 사용하는 것이 좋음
    • 하지만 크고 복잡한 객체가 필요하지 않은 경우, 작은 함수를 쓰는 것이 낫다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    // bad
    const Animal = function(age) {
    if (!(this instanceof Animal)) {
    throw new Error("Instantiate Animal with `new`");
    }

    this.age = age;
    };

    // 함수 설정
    Animal.prototype.move = function move() {};


    // good
    class Animal {
    constructor(age) {
    this.age = age;
    }

    move() {
    /* ... */
    }
    }

    ///////////////
    // bad
    const Mammal = function(age, furColor) {
    if (!(this instanceof Mammal)) {
    throw new Error("Instantiate Mammal with `new`");
    }

    // call 함수를 이용해 age 전달
    Animal.call(this, age);
    this.furColor = furColor;
    };

    Mammal.prototype = Object.create(Animal.prototype);
    Mammal.prototype.constructor = Mammal;
    // 함수 정의
    Mammal.prototype.liveBirth = function liveBirth() {};

    // good
    class Mammal extends Animal {
    constructor(age, furColor) {
    // super() 을 이용해 상속
    super(age);
    this.furColor = furColor;
    }

    liveBirth() {
    /* ... */
    }
    }

    //////////////
    // bad
    const Human = function(age, furColor, languageSpoken) {
    if (!(this instanceof Human)) {
    throw new Error("Instantiate Human with `new`");
    }

    Mammal.call(this, age, furColor);
    this.languageSpoken = languageSpoken;
    };

    Human.prototype = Object.create(Mammal.prototype);
    Human.prototype.constructor = Human;
    Human.prototype.speak = function speak() {};

    // good
    class Human extends Mammal {
    constructor(age, furColor, languageSpoken) {
    super(age, furColor);
    this.languageSpoken = languageSpoken;
    }

    speak() {
    /* ... */
    }
    }
  1. Method Chaining 사용하기

    Method Chaining 이란?

    Named Parameter Idiom으로, OOP에서 여러 메소드를 이어서 호출하는 문법이다.

    각각의 메소드는 객체 ( this ) 를 반환하며, 여러 메소드를 순차적으로 선언할 수 있다. 이를 통해 중간 결과값을 따로 변수에 저장하지 않아도 하나의 문장 만으로 메소드를 이어서 호출할 수 있다. 따라서 장기적으로는 유지, 보수에 도움이 된다.

    Named Parameter Idiom이란?

    1
    File f = OpenFile("foo.txt", true, false);

    File 객체에 대한 설정을 생성자의 인자로써 넘기면 인자의 개수도 많아지고, 헷갈리기 쉽다. 이 때 Named Parameter Idiom 을 사용하면,

    1
    2
    3
    File f = OpenFile("foo.txt")
    .readonly()
    .appendWhenWriting();

    위와 같이 method chaining을 사용하여 간결하게 표현할 수 있으며, parameter의 위치를 기억할 필요가 없으므로 실수를 방지할 수 있다.

    • 객체 ( this ) 를 반환하여 연속적 (순차적) 으로 함수를 호출할 수 있음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}

// bad
setMake(make) {
this.make = make;
}

setModel(model) {
this.model = model;
}

setColor(color) {
this.color = color;
}

save() {
console.log(this.make, this.model, this.color);
}

// good
setMake(make) {
this.make = make;
// NOTE: Returning this for chaining
return this;
}

setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}

setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}

save() {
console.log(this.make, this.model, this.color);
// NOTE: Returning this for chaining
return this;
}
}

const car = new Car("Ford", "F-150", "red");
car.setColor("pink");
car.save();
// const car = new Car("Ford", "F-150", "red").setColor("pink").save();
  1. 상속보다는 조합을 사용하기

    • has-a 관계에서는 조합을 사용해야 함

    c.f ) 조합보다 상속을 이용하면 좋은 경우

    • 상속 관계가 has-a 가 아닌 is-a 일 때
      • 상속; Human → Animal : is-a
      • 조합; User → UserDetails : has-a
    • Base Class 의 코드를 재사용할 수 있을 때
      • Human can move like all  animals
    • Base Class를 수정 함으로써 이에 따른 클래스들을 전체적으로 변경하고 싶을 때
      • Change the caloric expenditure of all animals when they move.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // bad
    class Employee {
    constructor(name, email) {
    this.name = name;
    this.email = email;
    }

    // ...
    }

    // Bad because Employees "have" tax data.
    // EmployeeTaxData "is not" a type of Employee.
    class EmployeeTaxData extends Employee {
    constructor(ssn, salary) {
    super();
    this.ssn = ssn;
    this.salary = salary;
    }

    // ...
    }

    // good
    // extending보다는 다른 클래스로 생성하고
    class EmployeeTaxData {
    constructor(ssn, salary) {
    this.ssn = ssn;
    this.salary = salary;
    }

    // ...
    }

    class Employee {
    constructor(name, email) {
    this.name = name;
    this.email = email;
    }

    // Employee 클래스의 메소드를 통해 EmployeeTaxData 객체를 이용하여 this의 taxData 를 설정한다.
    setTaxData(ssn, salary) {
    this.taxData = new EmployeeTaxData(ssn, salary);
    }
    // ...
    }

Hooks


기본 Hook


useState

1
const [state, setState] = useState(initialState);
  • useState() : statesetState 을 반환하는 함수

    • initialState : 초기값
      • 보통 null 이나 "" 등의 default value로 설정한다.
  • state : 상태 유지 (저장) 값

    • 최초 렌더링 시 반환된 stateinitialValue 이다.
  • setState : state 값을 갱신하는 함수

    1
    setState(newState);
    • state 값을 받으면 컴포넌트 리렌더링을 큐에 등록한다.
    • 리렌더링 시 useState 가 반환하는 첫번째 값 (인자) 은 갱신된 최신 state 이다.

Note

React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.


Functional Updates

함수적 갱신


  1. setState 가 값을 갱신하는 과정을 직접 코드로 작성하면 다음과 같다.

    1
    2
    3
    4
    5
    const [state, setState] = useState({});
    setState(prevState => {
    // Object.assign would also work
    return {...prevState, ...updatedValues};
    });

    갱신된 객체를 자동으로 merge하는 useState 와 반대로, setStateinitialValue 를 유지한다.

    이전 state 를 이용해서 값을 갱신하는 경우, 이전 값을 받아 갱신된 값을 반환하는 함수를 setState 로 전달하면 된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function Counter ({ initialCount }) {
    const [count, setCount] = useState(initialCount);
    return (
    <>
    Count: {count}

    // 초기화
    <button onClick={() => setCount(initialCount)}>Reset</button>

    // 이전 값을 이용해서 값을 갱신하는 경우
    // parameter value : 현재 값
    // return value : 갱신할 값
    <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
    );
    }

Note

Another option is useReducer, which is more suited for managing state objects that contain multiple sub-values.


  1. 업데이트 함수 (setState에 전달한 함수) 가 현재 상태와 정확히 동일한 값을 반환한다면 바로 뒤에 일어날 리렌더링은 완전히 건너뛰게 된다.

React에서 {} 를 쓰는 경우

  • JSX 내부에서는 객체를 받아옴
    • {{}} : 객체를 받아오는 것이 아니라 JSX 내부에서 생성할 때
  • JSX 외부에서는 일반 JS의 문법 중 function destructurizing을 실행

Lazy Initial State

지연 초기 state


initialState 인자는 초기 렌더링 시에 사용하는 state (값) 으로, 리렌더링 시 이 값은 무시된다.

만약 초기 state가 고비용 계산의 결과라면, 초기 렌더링 시에만 실행될 함수를 정의하여 전달하는 것이 효율적이다.

1
2
3
4
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});

Bailing Out of a State Update

state 갱신의 취소


앞서 설명한 “업데이트 함수 (setState에 전달한 함수) 가 현재 상태와 정확히 동일한 값을 반환한다면 바로 뒤에 일어날 리렌더링은 완전히 건너뛰게 된다.” 를 추가적으로 설명하자면,

State Hook을 현재 state와 동일한 값으로 갱신 (update) 할 경우, 리액트는 children or firing effects를 렌더링하는 과정을 건너뛰고 실행 (처리) 을 종료한다.


Note

React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.


useEffect

함수형 컴포넌트 내에서는 React’s render phase에 따라 Mutations, subscriptions, timers, logging, and other side effects 가 허용되지 않는다.

이를 위해 useEffect 를 사용할 수 있다.

1
useEffect(didUpdate);
  • useEffect() : 모든 렌더링이 완료된 후 / 어떤 값이 변경되었을 때에만 동작 ( didUpdate ) 을 수행
  • didUpdate : 특정 effect를 발생시키는 (명령형) 함수

Cleaning Up an Effect

effect 정리


Effect는 컴포넌트가 화면에서 제거될 때 정리되어야 하는 요소 (e.g. subscription, timer ID 등) 를 만들곤 한다. 이를 정리하는 함수를 useEffect 에 전달할 수 있다.

1
2
3
4
5
6
7
8
useEffect(() => {
// new subscription is created on every update
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
subscription.unsubscribe();
};
});
  • clean-up function (e.g. subscription.unsubscribe() )은 컴포넌트가 제거되기(=== 다음의 effect) 이전 에 실행되어 memory leaks를 방지한다.
  • 컴포넌트가 여러번 리렌더링 될 경우, 이전의 effect는 다음의 effect가 실행되기 이전 에 정리된다.

Timing of Effects

effect 타이밍


useEffect에 전달된 함수의 경우 레이아웃 배치과 그리기를 완료한 후, 지연된 이벤트가 발생하는 동안 실행된다. 이는 브라우저가 화면을 업데이트하는 과정을 방해하지 않기 때문에, 여러 side effects (e.g. setting up subscriptions and event handlers) 를 실행하기에 적합한 때이다.

useEffect 는 브라우저가 그려지기 이전까지는 대기하지만, 새로운 렌더링이 발생하기 이전에 실행되는 것을 보장한다. 리액트는 언제나 새로운 업데이트가 발생하기 이전에 이전의 렌더링 effect를 모두 제거한다.


useLayoutEffect

DOM 변경과 같이 사용자에게 effect가 보여지는 경우, visual inconsistency 방지를 위해 다음 execution (event) 이 실행되기 이전에 현재 화면이 렌더링이 됨과 동시에 effect가 (synchronously) 발생하여야 한다.


Conditionally Firing an Effect

조건부 effect 발생


Effect의 default behavior은 렌더링이 모두 완료된 이후 실행되는 것이다. 이는 해당 effect의 dependency 중 하나가 변경될 때마다 새로 실행됨을 의미한다.

앞선 예제를 통해서 자세히 보면,

1
2
3
4
5
6
7
8
useEffect(() => {
// new subscription is created on every update
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
subscription.unsubscribe();
};
});

subscription 은 매 업데이트마다 생성될 필요 없이, source prop 이 변할 때만 새로 생성되면 된다.

이와 같은 현상을 방지하기 위해, useEffect 의 2번째 argument로 effect가 의존하는 (depend on, dependency) 배열을 전달한다.

따라서 위 예제를 수정해보면,

1
2
3
4
5
6
7
8
9
10
11
useEffect(
// 전달하는 함수는 동일하되,
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
// 두번째 인자로 dependency array (depend on하는 배열) 을 전달한다.
[props.source],
);

이를 통해 subscriptionprops.source가 변경될 때에만 재생성된다.


만약 effect를 (mount 또는 unmount 시) 한 번만 수행하고 싶다면 두번째 인자로 빈 배열 ( [] ) 을 전달하면 된다. 이를 통해 effect는 컴포넌트 범위에서 가져온 값들 (e.g. props, state 등) 에 전혀 의존하지 않으므로 해당 값들이 변경되어도 다시 실행되지 않는다.

빈 배열 ( [] ) 은 effect 안에 있는 propsstate 가 항상 초기값을 가지게 됨을 의미한다. 이는 componentDidMountcomponentWillUnMount 의 기능과 같다.


두번째 인자로 전달하는 배열은 effect에 사용되는 컴포넌트 범위의 모든 값 (e.g. props, state 등) 들을 포함해야 한다. 이를 위반할 시 이전 렌더링에서 설정한 값을 참조하는 버그가 발생한다.

이렇게 전달된 dependency 배열은 effect 함수에 인자로 전달되지 않는다. 하지만 effect 함수 내에서 참조된 모든 값들이 해당 배열에 포함되어야 하므로, 전달 여부는 유의미하지 않다.


useContext

1
const value = useContext(MyContext);
  • useContext() : ( React.createContext 를 통해 반환된 값인 ) context 객체에 **현재 context 값 (MyContext) **을 반환한다.
    • 가장 가까운 상위 컴포넌트 <MyContext.Provider> 가 업데이트 되면, useContext 는 해당 컴포넌트에 전달 (포함) 된 가장 최신의 context value 에 따라 화면을 리렌더링 한다.
  • MyContext : 현재 context 값 (객체) 으로, 트리 구조에서 해당 컴포넌트의 상위 컴포넌트이면서 가장 가까운 <MyContext.Provider>value prop 에 따라 결정된다.
    • useContext() 의 인자는 context 객체여야 한다.
      • Correct: useContext(MyContext)
      • Incorrect: useContext(MyContext.Consumer)
      • Incorrect: useContext(MyContext.Provider)

useContext 를 사용한 컴포넌트는 context 값이 변경될 때마다 리렌더링된다. 리렌더링 되는 컴포넌트가 비싸다면, momoization 을 이용해 효율성을 높일 수 있다.

상위 컴포넌트에서 React.memoshouldComponentUpdate 를 쓰더라도, useContext 를 사용한 컴포넌트부터 리렌더링이 진행 (시작) 된다.


useContext 는 context를 읽고 변경사항을 구독 (확인) 하는 것만 가능하기 때문에, 해당 context의 값을 전달하기 위해 트리 구조 내 상위 컴포넌트로서 <MyContext.Provider> 가 필요하다.


Putting it together with Context.Provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};

const ThemeContext = React.createContext(themes.light);

function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}

function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}

function ThemedButton() {
// useContext의 인자로서 ThemeContext 객체 전달
// theme = <ThemeContext.Provider>의 value인 {themes.dark} // 객체
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}



추가 Hooks


useReducer

useState() 의 대체재로서 사용한다. 여러 하위 값을 포함하거나 다음 state가 이전의 state 값에 영향을 받는 복잡한 state 로직을 가진 경우 선호된다.

컴포넌트의 state 업데이트 로직을 컴포넌트에서 분리하여 컴포넌트 바깥에서 작성하거나 다른 파일에서 작성한 후 불러와서 사용할 수도 있다.

또한 callback 대신 dispatch를 전달한 수 있어 deep update를 요구하는 컴포넌트의 경우 performance를 최적화할 수 있다.

What is deep update in React?

먼저, React에서 update란; useState() 를 사용하는 모든 리액트 컴포넌트에서 사용가능한 함수로, 컴포넌트 state가 변경되었을 때 리액트에게 이를 알림으로써 해당 state에 종속된 UI를 업데이트하기 위해 컴포넌트에게 리렌더링이 필요하다는 것을 전달한다.

따라서, deep updatedeep nested 객체 (many pieces of information과 fixed 스키마를 가진) 를 update 하는 것을 의미한다.


1
const [state, dispatch] = useReducer(reducer, initialArg, init);
  • useReducer() : reducer 를 받아 현재 state와 짝지어진 dispatch 함수를 반환한다.

  • reducer : 현재 stateaction 객체를 파라미터로 받아와서 ( (state, action) ) 새로운 state를 반환하는 함수

    • (state, action) => newState 의 구조를 가진다.

      1
      2
      3
      4
      5
      function reducer(state, action) {
      // 새로운 state를 만드는 로직
      // const nextState = ...
      return nextState;
      }
    • 반환하는 해당 state는 컴포넌트가 지닐 새로운 state가 된다.

    • action : 업데이트를 위한 정보를 가지고 있다.

      • 주로 type 값을 지닌 객체 형태로 사용한다.

        type 의 경우, 대문자 를 사용하거나 _ 로 시작하는 관습이 있다.

  • state : 컴포넌트에서 사용할 수 있는 상태

  • dispatch : action 을 (인자로 받아) 발생시키는 함수

    1
    dispatch({ type: 'INCREMENT' })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const initialState = {count: 0};

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}

// dispatch의 인자로 action (type) 전달
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

Note

리액트는 리렌더링이 되더라도 dispatch 함수의 identity 는 동일하게 유지하기 때문에, useEffect 나 useCallback 의 dependency list에 포함하지 않는다.


Specifying the Initial State

초기 state의 구체화


useReducer() 의 state를 초기화하는 방법에는 2가지가 있다.

  1. 가장 간단한 방법으로는, initial state를 2번째 인자로 전달하는 방법이 있다.
1
2
3
4
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);

Lazy Initialization

초기화 지연


  1. initial state를 lazily 생성하는 방법으로, init 함수를 3번째 인자로 전달하여 initial state를 init(initialArg) 로 설정할 수 있다.
    • initial state 를 계산하는 로직을 reducer 밖에 정의하여 action 에 따른 state 재설정 시 유용하게 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// initialization 함수를 외부에 정의
function init(initialCount) {
return {count: initialCount};
}

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};

// 재설정 시 init 함수 호출
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}

function Counter({initialCount}) {

// 3개의 인자 전달 (reducer, initial_value, initialization_function)
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}

// 재설정 시 action 객체에 {type, payload} 전달
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

payload

전송되는 데이터


Bailing Out of a Dispatch

dispatch의 회피


앞서 useState() 에서 설명한 것과 같이, Reducer Hook ( useReducer() ) 이 현재 state과 동일한 값을 반환할 경우, 자식을 렌더링하거나 effect를 실행하지 않고 처리 (i.e. 렌더링, 이벤트) 를 종료한다.

Object.is() comparison 알고리즘을 이용

처리를 종료하기 전에 해당 컴포넌트를 다시 렌더링해야 하는 경우가 존재하지만, 그 이상으로 깊게 트리를 탐색하지 않으므로 문제가 되지는 않는다. 만약 렌더링에 비싼 계산 과정 이 포함된다면, useMemo() 를 사용할 수 있다.


useCallback

memoized callback 을 반환한다.


memoization

  1. 비싼 함수의 호출 결과를 저장하고 같은 input이 발생할 경우 저장된 (캐시) 값을 반환하여 프로그램의 수행 속도를 증가시키는 최적화 (optimization) 기술

  2. (some sort of) 캐시를 이용하여 (비싼) 함수를 부르는 횟수를 제한하는 것

    • 특정 input에 대한 캐시값이 없다면, original 함수가 한번 호출되어 (캐시) map에 결과값이 추가된다.
    • 캐시를 지우지 않는 한 결과는 캐시 map에서 반환된다.

👉 useCallback hook 은 다른 기능을 한다.

🌈 REFERENCE를 꼭 읽어보세염 (:->

useCallback() 을 이용하는 이유는, callback 함수인 cbmemoized 버전을 받기 위해서이다.

컴포넌트가 처음으로 렌더링될 때, 새로운 cb 가 생성되고 똑같은 cb 함수가 useCallback() 에 의해 반환된다.


위 예시에서 memoizedCb() 가 호출되면, original cb 콜백함수가 호출되어 결과적으로 original cb 함수를 호출한다.

컴포넌트가 리렌더링 (2번째) 되면, 새로운 cb 가 생성된다. 하지만 useCallback() 에 전달된 dependency가 변하지 않았기 때문에, 생성된 cb는 버려지고 memoizedCb는 첫번째 렌더링 때 생성된 cb를 유지한다. 결국 이 때의 memoizedCb 호출은 이전과 같이 기존의 (old) cb 를 호출하여 original cb 함수를 호출하게 된다.

dependency가 변하지 않는 이상, 모든 렌더링은 위와 같은 과정을 반복하여 사용하지 않는 콜백함수를 생성하고 기존의, memoized 콜백을 호출하여 결국 original cb 함수를 호출한다.


dependency 가 변한 경우, memoizedCb 도 변한다.

이전의 memoizedCb 가 첫번째 cb를 호출하는 과정과 비슷하게, 이 때의 memoizedCb 또한 dependency 가 변한 시점부터 첫번째 cb 를 호출한다.


callback

특정 함수 (콜백함수) 를 다른 함수에 전달하고 이를 호출하는 특정 이벤트 (e.g. a query finished or an error occurs) 가 발생하면 콜백함수를 발생시키는 것

  • 여러 조각의 코드의 소통을 정의하여, 콜백함수를 호출하는 모듈이 결과를 사용하는 것이 아니라 이벤트가 발생했을 때 호출을 받는 함수에 콜백함수를 전달한다.
  • 여기서 중요한 점은, one piece of code가 콜백함수를 호출하면 다른 (조각의) 코드가 이 호출을 받는다는 것이다. 여기서 어떠한 호출 코드도 잃으면 (생략되면) 안 되기 때문에, 콜백함수를 memoizing하여 모듈 간 소통 line을 끊는 방법이 탄생한 것이다.

결과적으로 useCallback() 이 하는 것은 똑같은 callback 의 횟수를 제한하는 것이다. 결과값들은 캐시에서 반환되지 않고, dependency 가 변한 시점으로부터 첫번째의 cb 와 같은 memoizedCb 를 반환한다.


1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
  • useCallback() : inline callback 함수와 dependencies 배열을 전달
    • memoized version of the callback을 반환하며, dependencies가 변했을 때만 memoized version 도 변경된다.
    • useCallback(fn, deps) === useMemo(() => fn, deps)

불필요한 렌더링을 방지하기 위해 reference의 동일성에 의존하는 (동일성에만 영향을 받는) (최적화된) 자식 컴포넌트에 콜백함수를 전달할 때 유용하다.

앞선 useEffect와 마찬가지로, dependencies 배열은 콜백함수에 인자로 전달되지 않는다.

하지만 정의 자체가, “콜백함수에서 참조하는 모든 값들은 dependencies 배열에 포함되어야 한다” 이다.


useMemo

memoized 값을 반환한다.

useMemo vs. useCallback

useCallback() : 함수 반환

useMemo() : 값 반환


1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • useMemo() : create 함수dependencies 배열 을 전달
    • dependency 가 변할 때에만 memoized 값을 다시 계산한다. 이를 통해 비싼 계산 과정이 매 렌더링마다 발생하는 것을 방지한다.
    • 2번째 인자 (dependencies 배열 로서) 에 빈 배열 ( [] ) 이 전달되면, 매 렌더링 마다 새로운 값이 계산된다.

useMemo() 에 전달된 create 함수렌더링 동안 실행된다.

보통 side effect는 useMemo 가 아니라 useEffect 에서 발생한다. 따라서 렌더링 동안은 다른 행동을 하지 않아야 한다. (당연함 -.-)


❗️useMemo() 는 성능 최적화에만 사용하고, 큰 의미적 기능 (semantic guarantee) 을 담지 않아야 한다. 따라서 useMemo() 를 사용하지 않고도 동작할 수 있도록 코드를 작성하고, 성능 최적화를 위해 useMemo() 를 추가하도록 한다.

앞선 useEffect, useCallback 과 마찬가지로, depedencies 배열 은 콜백함수에 인자로 전달되지 않는다.


useRef

JavaScript를 이용해 특정 DOM 을 선택해야 하는 경우 getElementById, querySelector 와 같은 DOM Selector 함수 를 사용한다.

리액트를 사용할 때도 특정 DOM 을 선택해야 하는 상황이 발생하는데, 이 때 ref 를 사용한다.

  • 함수형 컴포넌트에서 ref 를 사용 할 때에는 useRef 라는 Hook 함수 를 사용한다.
  • 클래스형 컴포넌트에서는 콜백 함수를 사용하거나 React.createRef 라는 함수를 사용한다.

1
const refContainer = useRef(initialValue);
  • useRef() : (생성된 ref 객체의) .current 프로퍼티 (속성) 가 전달된 인자 (e.g. initialValue) 로 초기화된, 변경 가능한 ref 객체를 반환한다.
    • 변경가능한 값을 넣을 수 있는 .current 프로퍼티를 갖고 있는 “박스” 와 같다.
    • 해당 객체는 컴포넌트의 full lifetime 동안 지속된다.

useRef()DOM에 접근하는 방법으로 많이 사용된다.

useRef() 를 사용하여

  1. Ref 객체를 만들고,
  2. 이 객체를 우리가 선택하고 싶은 DOM (노드) 에 ref 값으로 설정한다.
  3. 그러면, Ref 객체의 .current 값은 우리가 원하는 DOM 을 가르키게 된다.

1
<div ref={refContainer} />

refContainer.current 프로퍼티를 해당 ref 객체 ( refContainer ) 를 ref 로 설정한 DOM 노드로 설정하고, 노드가 변경될 때마다 그에 맞게 .current 프로퍼티를 조정한다.

하지만 useRef().current 에 변경 사항이 존재하더라도 리렌더링 하지 않기 때문에, 이에 대해 notify 하지 않는다. 만약 리액트가 DOM 노드에 ref 객체를 연결하거나 제거할 때 notify 하고 싶다면, callback ref 를 이용해야 한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function TextInput_With_FocusButton() {
// inputEl : ref 객체 (null로 초기화됨)
const inputEl = useRef(null);

// button을 클릭하면 inputEl.current 인 input node에 focus() 를 적용한다.
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
// inputEl ref 객체를 ref로 설정하여 inputEl.current 프로퍼티를 input node 로 설정한다.
<input ref={inputEl} type="text" />

// onClick 시 onButtonClick 함수를 실행하고 // 👉 goto onButtonClick comment
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

HTMLElement.focus()

특정 element 에 (focus를 맞출 수 있을 때) focus를 맞추는 함수로, 해당 element는 키보드 등의 이벤트를 받는 default element로 정의된다.




useRef() 는 ref 속성 외에도, 클래스가 instance field (인스턴스 변수) 를 가지고 있는 것처럼 ref 객체에 변경 가능한 어떠한 값도 담을 수 있다는 장점이 있다.

이는 useRef()plain JavaScript 객체 를 생성하기 때문인데, 직접 {current: ...} 로 객체를 생성하는 것과 유일한 차이점은 useRef() 는 매 렌더링마다 똑같은 ref 객체를 제공 (반환) 한다는 것이다.

class field (클래스 변수) vs. instance field (인스턴스 변수)

  • 클래스 변수 : 여러 인스턴스 (서로 다른 객체) 간에 공유해야 하는 값을 바인딩
  • 인스턴스 변수 : 각 인스턴스 (객체) 마다 가지고 있는 고유한 값

파이썬은 인스턴스를 통해 접근한 이름 (변수) 이 인스턴스의 name_space (name들을 정의한 공간) 에 없을 경우, 그 다음으로 클래스의 name_space에서 찾아본다.

참고로 클래스 변수에 접근할 때에는 클래스 이름을 사용하여 바로 변수 (값) 에 접근할 수 있다.


useImperativeHandle

❗️ref를 사용하는 imperative code 는 되도록이면 피하는 것이 좋다. ):-<

ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값 (변수) 을 커스터마이징 한다.

즉, forwarding된 ref를 replace할 수 있는 기능을 제공한다.


1
useImperativeHandle(ref, createHandle, [deps])
  • useImperativeHandle() : ref, 콜백함수 (createHandle), dependnecies 배열 을 전달

    • createHandle() : 현재 컴포넌트의 값을 부모 컴포넌트가 접근할 수 있도록 하는 콜백함수
  • forwardRef() 와 함께 사용되어야 한다.

    forwardRef()

    (말 그대로) reference를 전달해주는 기능을 하는 함수


1
2
3
4
5
6
7
8
9
10
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

<FancyInput ref={inputRef} /> 를 수행 (렌더링) 하는 부모 컴포넌트는 inputRef.current.focus() 를 호출할 수 있다.


useLayoutEffect

useEffect 와 동일한 기능을 DOM 변경 시 동시에 (synchronously) 실행하는 함수로, DOM 에서 레이아웃을 읽음과 동시에 리렌더링을 해야할 때 사용한다.

useLayoutEffect 내부에 정의된 예정된 업데이트 또한 브라우저가 레이아웃을 그리기 이전에, 읽음과 동시에 발생한다.

화면 업데이트를 차단하지 않아도 되는 경우에는 useEffect 를 사용하는 것이 좋다.


클래스 컴포넌트에서 코드를 옮길 때 useLayoutEffect()componentDidMountcomponentDidUpdate 와 같은 단계에서 발생한다. 하지만, 먼저 useEffect 를 시도해보고 에러가 발생할 때 useLayoutEffect 를 사용하는 것이 좋다.

SSR (서버 사이드 렌더링) 의 경우, useLayoutEffectuseEffect 는 JavaScript 가 모두 다운되기 전까지는 실행되지 않는다. SSR 컴포넌트가 useLayoutEffect 를 포함한 경우,

  • 첫 렌더링 시 해당 컴포넌트가 필요하지 않다면 useEffect 로 로직을 옮기고,
  • useLayoutEffect 가 실행되기 전까지 HTML이 망가져 보인다면 클라이언트 렌더링이 완료될 때까지 해당 컴포넌트를 보여주는 것을 딜레이시킨다.

서버 렌더링 HTML에서 레이아웃 effect를 필요로 하는 컴포넌트를 제외하기 위해서,

  • showCild && <Child /> 를 이용하여 해당 컴포넌트를 조건적으로 렌더링을 하고,
  • useEffect(() => { setShowChild(true); }, []) 를 이용하여 보이는 것을 미룬다 (defer).

이를 통해 HTML이 hydration (useLayoutEffect, useEffect 실행) 이전에 망가져 보이는 것을 방지한다.


useDebugValue

1
useDebugValue(value)

React DevTools 에서 커스텀 Hooks의 label (value) 을 보여준다.


1
2
3
4
5
6
7
8
9
10
11
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);

// ...

// Show a label in DevTools next to this Hook
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');

return isOnline;
}

모든 커스텀 Hook에 debug value를 추가하는 것은 좋지 않다.

shared libraries에 속하는 커스텀 Hooks에 유용하게 사용될 수 있다.


Defer Formatting Debug Values

디버그 값 포맷팅 지연하기


디스플레이 (되는) 값을 포매팅하는 것은 고비용 연산일 수 있고, Hook이 감지되지 않은 경우 사실상 포매팅은 불필요하다. 따라서, useDebugValue 의 (optional한) 2번째 인자로 포매팅 함수 를 전달하여 Hook이 감지되었을 때만 해당 함수를 호출하여 포매팅할 수 있다.


1
useDebugValue(value, value => value.formatting_function());
  • useDebugValue() : valueoptional formatting 함수 를 전달
    • optional formatting function : debug value 를 인자로 전달하고 formatted display value 를 반환한다.

1
useDebugValue(date, date => date.toDateString());

2번째 인자로 fomatting function 을 전달하여 Date value 를 반환하는 커스텀 Hook이 toDateString()불필요하게 호출하는 것을 방지한다.




Reference

https://ko.reactjs.org/docs/hooks-reference.html

https://reactjs.org/docs/hooks-reference.html

들어가기에 앞서

Framework

개발에 필요한 뼈대를 제공하여 사용자가 덧붙이는 형식으로 코드를 완성시킨다.

e.g. 집


Library

개발에 필요한 도구들을 제공하여 사용자가 필요할 때 리소스를 가져와 사용한다.

e.g. 가구


👉 둘 다 공통의 개발환경을 제공 하여 개발 시간과 리소스 비용을 줄여주는 도구


React 이해하기


React는…

  • 지속해서 데이터가 변화하는 대규모 애플리케이션 을 구축하기 좋다.
  • 점유율이 가장 높다.


JQuery

$ 이용

  1. 자바스크립트 라이브러리
  2. DOM 조작을 쉽고 효율적으로
  3. 브라우저마다 자바스크립트 호환성이 달랐던 문제를 해결

👉 ES6로 인해 사용률 ↓



SPA (Single Page Application) 의 등장

  1. 웹의 규모가 커지면서 단순한 웹 페이지가 아닌, 웹 어플리케이션 으로 발전
  2. 엄청난 JS 파일을 유지, 보수하기 위해 웹 라이브러리/ 프레임워크의 중요성 대두
  • 웹 페이지 전체를 재로딩하지 않고, 필요한 데이터만 동적으로 로딩하는 웹 어플리케이션
  • 기존의 MPA (Multiple Page Application) 는 페이지를 이동할 때마다 HTML, CSS, JS 파일을 전부 로딩해야 해서 속도가 상대적으로 느리다.
  • SPA는 필요한 정적 파일을 최초 1회 다운 받은 후, 필요한 데이터만 서버로부터 동적으로 전달받아 DOM을 조작한다.
장점 단점
속도가 빠르고 새로고침이 발생하지 않음 처음에 필요한 모든 정적 리소스를 다운로드하기 때문에 초기 구동 속도가 상대적으로 느림
처리과정이 효율적임 데이터가 AJAX를 통해 로드되기 때문에 검색 엔진 최적화가 어려움
👉 SSR (Server Side Rendering) 으로 해결 가능함
네트워크 동작을 모니터링 할 수 있어 디버깅 하기가 쉬움
네이티브 앱과 유사한 사용자 경험을 제공함으로써 UX를 향상시킬 수 있음

SPA Web Framework

  1. Angular
    • Framework에 가까움
    • TS 기반, 양방향 바인딩 지원
  2. React
    • Library에 가까움
    • Native로 앱 개발도 가능함 (React Native)
    • Virtual DOM, JSX
  3. Vue
    • 오픈소스 프로젝트
    • 가장 최근에 개발됨
    • Virtual DOM, 양방향 바인딩 지원, Single File Component

React 기본 문법 익히기

  • Component

    • UI 를 구성하는 개별적인 뷰 단위
      • 적절하게 분리하여 재사용성을 높이는 게 좋다. (너무 많아도/ 적어도 안 좋다!)
    • 컴포넌트를 조합하여 새로운 컴포넌트를 만들거나, 복잡한 웹 페이지를 구성할 수 있다.
    • 클래스형/ 함수형 2가지 종류의 컴포넌트가 있다.
  • Virtual DOM

    Screenshot 2021-04-24 at 15 26 29

    • DOM 요소가 변경되면, Render Tree를 만들어서 다시 화면에 보여주기까지 연산이 오래 걸린다.

    • 데이터 변경이 자주 발생하는 웹 페이지는 속도가 느려진다.

      👇 변경된 부분을 한 번에 처리하자! 🚀

    • 가상 DOM을 통해 변경된 부분만 탐지하여 실제 DOM에 적용

      • 연산을 최소화하여 성능이 개선됨
  • One-way Data Flow

    바인딩은 뷰에 보여지는 값메모리 상의 값이 일치하는지 여부에 따라 방식이 달라진다.


Screenshot 2021-04-24 at 15 29 25

  • React는 기본적으로 단방향 데이터 바인딩 이다.

    • 단순하고 예측 가능한 구조

    • 뷰에서 사용자가 입력한 값이 변수에 반영 되기 위해서는 event 를 거쳐야 한다.

      e.g. onChange()

  • Two-way Data Flow

    Screenshot 2021-04-24 at 15 30 02

    • 양방향 데이터 바인딩에서는 사용자가 입력한 값이 바로 변수에 반영된다.
  • JSX
    • JavaScript를 확장하여 React의 요소를 생성하는 문법
    • HTML과 JavaScript를 결합한 형태이며, 중괄호 {} 안에 모든 JS 문법을 사용할 수 있다.


Node.js

  • npm 패키지 매니저를 사용하기 위해서 필요한 라이브러리

  • React 패키지가 포함되어 있음


CRA (Create React App)

React를 사용하기 위해 폴더를 초기화하는 과정이 필요함

1
npx create-react-app [fileName]

Functional Component

src > App.js에서 시작


  • 컴포넌트를 만들 때는 react를 import 해야 한다.

    1
    import React from "react"
  • 함수형 컴포넌트의 이름은 반드시 대문자로 시작해야 한다.

  • return 값이 실질적으로 화면에 보여지는 내용이다.

  • return 값의 최상위에는 하나의 요소만 있어야 한다.

    • <><React.Fragment> 와 동일하며, return 값의 최상위에 의미 없는 <div> 를 추가하지 않도록 사용한다.
    1
    2
    3
    <>
    ...
    </>
  • 다른 컴포넌트에서 사용하기 위해서는 export 해야 한다.

    1
    export default [name];
  • 최종적으로 App.js 에서 컴포넌트를 import하고, <> 를 통해 요소로써 사용한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import [name] from './[file_path]';

    function App() {
    return (
    <>
    <[name] />
    </>
    );
    }
  • 컴포넌트는 여러번 반복해서 사용할 수 있다.


JSX

  • JSX는 기본적으로 객체 이며, React 요소를 나타낸다.

  • HTML 요소 내부에 JavaScript를 사용할 때는 중괄호 {} 내부에 변수를 넣는다.

  • HTML 요소 외부에서는 JavaScript를 자유롭게 사용할 수 있다.

  • HTML attribute의 value에도 사용할 수 있다.

  • JavaScript 기반이기 때문에 attribute 이름은 CamelCase로 작성한다.

  • inline style은 두가지 방법을 사용할 수 있다.

    • 직접 지정 : 객체이기 때문에 {} 2번 사용해야 함!
    • 변수로 지정

    변수 : {}

    객체 : {{}}


Props & State

React에서 다루는 데이터의 종류


Screenshot 2021-04-29 at 23 33 02
  • Props 는 정적 데이터로서, 부모 컴포넌트에서 자식 컴포넌트로 데이터를 넘겨주는 방식이다.
  • Props를 넘겨줄 때는 넘겨줄 자식변수명={부모(본인) 변수명}으로, 받을 때는 parameter 에 전체를 props 로 받아오거나 구조분해할당 하여 {자식변수명} 으로 받아온다.
    • 같은 이름으로 지정해서 넘겨주는 것이 좋다.
  • State동적 데이터를 다루는 방식으로서, useState라는 Hook 을 통해서 사용한다.

LifeCycle

Component의 생명 주기


Screenshot 2021-04-24 at 16 23 37

  • 클래스형 컴포넌트는 생명 주기에 따라서 다음 메소드가 실행된다.
  • componentDidMount : 최초 렌더링 직후
  • componentDidUpdate : 변경된 내용 렌더링 직후
  • componentWillUnmount : DOM에서 제거될 때

👇

함수형 컴포넌트useEffect 를 이용하여 한 번에 처리 가능함


Hooks

함수형 컴포넌트에서 State와 LifeCycle을 다룰 수 있는 기술


  • 클래스형 컴포넌트에서는 constructor 내부에서 this.state를 통해 state를 저장하고 관리한다.
  • componentDidMount, componentDidUpdate가 컴포넌트 생명주기에 따라 호출된다.

useState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from "react"; // 가져오기

// count : getter
// setCount : setter
// useState() === React.useState()

const [ _var, setVar ] = useState(initVal); // 선언하기

function useState(initialValue) {
let _var = initialValue;

function getVar () {
return _count;
}

function setVar(newValue) {
_var = newValue;
}
}
  • _var읽기만 가능한 변수
  • setVar(newVal)변수(_var)를 변경할 수 있는 함수

useEffect

1
2
3
import { useEffect } from "react";

useEffect(callBackFunc, dependencies);
  • useEffect의…
    • deps가 비어있다면 렌더링 될 때마다 초기화
    • deps가 [] 라면 최초 렌더링 시 에만 초기화
    • deps에 state의 배열이 들어가면 update; 최초 렌더링 + 해당 state가 변경되었을 때만 실행

DOM (Document Object Model)

1. HTML 코드 (Page Source 코드) === DOM?

코드 자체는 아님.

하지만 해당 코드가 브라우저에 파싱되면 DOM 임.

2. DevTools 코드 === DOM?

맞음.

기존에 작성한 HTML 코드와 동일할 수도 있지만, 대게 브라우저에 파싱됨에 따라 변경사항이 적용되어 브라우저에 보여지는 형태로 변형되어 보여짐.

  • 웹 페이지는 일종의 문서(document)

    • The backbone of an HTML document is tags
  • 웹 페이지의 객체 지향 표현 (독립적인 객체) 이며, 자바스크립트와 같은 스크립팅 언어를 이용해 DOM 을 수정

  • HTML, XML 문서의 프로그래밍 interface 로, 프로그래밍 언어가 DOM 구조에 접근할 수 있는 방법을 제공한다.

    • DOM 은 문서를 표현하고, 저장하고, 조작하는 (문서 구조, 스타일, 내용 등을 변경) 방법을 제공한다.
    • 웹 페이지를 스크립트 또는 프로그래밍 언어들에서 사용될 수 있게 연결시켜주는 역할
  • 문서의 구조화된 표현(structured representation)

    • node, property, method 를 갖고 있는 object들로 구조화된 문서를 표현

    • document object 는 document 자체

      e.g. table object 는 HTML table 에 접근하기 위한 HTMLTableElement DOM interface를 구현하는 것이다.

  • 동일한 문서를 사용하여 다른 형태로 나타날 수 있다.

    • 웹 브라우저를 통해 그 내용이 파싱(해석)되어 웹 브라우저 화면에 나타나거나,
    • 기존 HTML 소스 자체로 나타나기도 한다.
  • Everything in HTML, even comments, becomes a part of the DOM.

DOM과 기존 HTML 코드가 달라지는 경우

  1. 브라우저가 HTML을 수정하는 경우 : 브라우저 파싱 시 자동 교정 (Autocorrection)

    1. Spaces and newlines before <head> are ignored for historical reasons.
    2. If we put something after </body>, then that is automatically moved inside the body, at the end, as the HTML spec requires that all content must be inside <body>. So there can’t be any spaces after </body>.
      • if there are spaces (just like any character) in the document, then they become text nodes in the DOM
    3. Tables always have <tbody>
  2. JavaScript로 DOM을 조작하는 경우 : 조작 (변경) 사항이 반영되어 DOM 출력

  3. Ajax : 컨텐츠를 가져와 (c.f. fetch) 페이지에 추가하는 경우

  4. Templating : server의 내용을 테스트하기 위해 임의의 client-side template을 적용

    e.g. handlebars.js

DOM과 JavaScript

JavaScript : 브라우저가 읽고 작업을 할 수 있는 언어, 브라우저에 기록되는 것들을 조작할 수 있는 문법/ 언어

  • DOM API 를 이용해서 작업

DOM : 작업이 이루어지는 장소, 브라우저에 의해 기록되는 모든 것

  • DOM 이 없다면 자바스크립트 언어는 웹 페이지 또는 XML 페이지의 요소들과 관련된 모델이나 개념들에 대한 정보를 갖지 (불러오지) 못하게 된다.

  • 문서의 모든 element는 DOM의 한 부분

  • 페이지 콘텐츠 (element) 는 DOM 에 저장되고 자바스크립트를 통해 접근하거나 조작

    1
    API (web or XML page) = DOM + JS (scripting language)
  • DOM 의 구현은 어떠한 언어에서도 가능

    e.g. python 에서도 구현 가능

DOM에 접근하는 방법

  • 문서가 로드될 때는 모든 DOM을 사용할 수 있게 되는 때이다.

  • 스크립트를 작성할 때

    • 직접적으로 문서 자체를 조작하거나,
    • 간접적인 조작을 위해 문서의 children 을 얻는, document 또는 window element 를 이용하는 API 를 사용할 수 있다.
    1
    <body onload="window.alert('welcome to my home page!');">
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <html>
    <head>
    <script>
    // run this function when the document is loaded
    window.onload = function() {

    // create a couple of elements in an otherwise empty HTML page
    var heading = document.createElement("h1");
    var heading_text = document.createTextNode("Big Head!");
    heading.appendChild(heading_text);
    document.body.appendChild(heading);
    }
    </script>
    </head>
    <body>
    </body>
    </html>

Data Type

Tags are element nodes (or just elements) and form the tree structure.

  • DOM nodeelement 로,

    • document – the “entry point” into DOM.
    • element nodes – HTML-tags, the tree building blocks.
    • text nodes – contain text.
    • comments – sometimes we can put information there, it won’t be shown, but JS can read it from the DOM.
  • 노드의 array는 nodeList (또는 element),

  • attribute 노드는 attribute

document document type 의 object 를 리턴할 때, 이 object 는 root document object 자체이다.
예를 들어 element의 ownerDocument property 는 그것이 속해 있는 document 를 return 한다.
element element 는 DOM API에 의해 리턴된 element 또는 element type 의 DOM node 를 의미한다.
예를 들어 document.createElement() 는 node를 참조하는 object 를 리턴하며, DOM 안에서 생성되는 element를 리턴한다고 말할 수 있다.
nodeList nodeList 는 element들의 배열이다. document.getElementsByTagName() 와 같은 method에 의해 리턴된 nodeList의 Item들은 index 를 통해 접근 가능하며, 다음과 같이 두 가지 방식이 있다 (동일한 방식임) :
- list.item(1)
- list[1]
item()nodeList object 의 단일 method 이다.
두번째 방식은 list 에서 두번째 item 을 fetch 하는 전형적인 array syntax 이다.
attribute API에 의해 attribute가 리턴되는 것은 attribute에 대한 특별한 인터페이스를 노출하는 object reference 가 표현됨을 의미한다. attribute는 DOM 에서 element와 같은 node이다.
namedNodeMap namedNodeMap 는 array 와 유사한 list로, item은 name 또는 index 에 의해 접근 가능하다. 리스트는 특별한 정렬이 적용되지 않았기 enumeration (열거, 목록화) 할 때 index 를 주로 사용한다. namedNodeMap 는 이를 위해 item() method 가 있으며, namedNodeMap 에 item 을 추가하거나 삭제할 수 있다.

DOM interface

  • object 와 DOM 에서 조작 가능 한 것
  • API 를 이용해 접근 및 조작

Interface와 Object

  • Every tree node is an object.

  • 많은 object가 여러 개의 다른 interface와 연관되어 있다.

  • HTML element 는 DOM 이 연관되어 있는 한 node 트리에서 하나의 node를 담당한다.

    • Node : 기본적인 Element
  • node 트리는 웹 페이지 또는 XML 페이지를 위한 object model을 구성한다.

1
table object 는 createCaption, insertRow method 들이 포함된 HTMLTableElement 을 구현한 것이다
  • table ElementNode interface 를 구현하고 있다.
  • table object 는 HTML Element (object model) 이기도 하기 때문에, tableElement interface도 구현한다.
  • table object 를 참조하게 되면, 기본적으로 이 3 가지 interface를 사용할 수 있다.
1
2
3
4
5
6
7
8
9
var table = document.getElementById("table"); // table === node/ element
var tableAttrs = table.attributes; // Node/Element interface
for (var i = 0; i < tableAttrs.length; i++) {
// HTMLTableElement interface: border attribute
if(tableAttrs[i].nodeName.toLowerCase() == "border")
table.border = "1";
}
// HTMLTableElement interface: summary attribute
table.summary = "note: increased border";

핵심 Interface

Documentwindow object 는 DOM 프로그래밍에서 가장 자주 사용하는 object

  • window object 는 브라우저와 같다.
  • document object 는 root document 자체이다.
DOM 을 사용하는 공통적인 API
  • document.getElementById(id)
  • document.getElementsByTagName(name)
  • document.createElement(name)
  • parentNode.appendChild(node)
  • element.innerHTML
  • element.style.left
  • element.setAttribute()
  • element.getAttribute()
  • element.addEventListener()
  • window.content
  • window.onload
  • window.dump
  • window.scrollTo

Interaction with console

  1. select the particular element (e.g. <li>) and press Esc opens the console

    • or just run inspect(node)
  2. the last selected element is available as $0, the previously selected is $1 etc.

  3. in console tab, run the command on the element

    1
    2
    // changes the background color of the inspected <li> element
    $0.style.background = 'red'

Reference

https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction

https://javascript.info/dom-nodes