13) 자바스크립트 X 클로저 (vs class, react hooks)

0. Intro

앞선 포스트들에서

  1. 생성자함수를 통해 인스턴스 객체를 만들고
  2. 생성자함수에서는 this를 통해 생성될 인스턴스 객체의 상태를 정하며,
  3. 인스턴스 객체는 생성자함수의 prototype 원형객체를 상속받는다는 것과
  4. 인스턴스 객체는 constructor로 생성자함수를 참조한다는 것과
  5. class (ES6)를 통해 더 쉬운 prototype 기반의 상속이 가능해졌다는 것
  6. 리액트 클래스 컴포넌트에서는 state의 비교를 통한 상태 변경을 한다는 점 (❓ 확실히 다시보기)

을 알아봤다!

하지만 자바스크립트 문법에는 클로저라는 메커니즘이 존재해서 이를 통한 상태관리가 가능하다.

1. 클로저

클로저 안에 정의된 함수는 만들어진 환경을 ‘기억한다’

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)의 조합이다.

출처: MDN

클로저는 함수가 속한 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능을 뜻한다.

Kyle Simpson

2. 클로저 vs 클래스

예제 출처 : Javascript Classes v. Closures (1/3) - Richard Marmorstein

2-1. 클래스방식

const Earth = require('earth')
const Society = require('society')
// An ES6 class!
class Bourgeoisie {
  constructor(land, capital) {
    this.land = land
    this.capital = capital
  }
  exploitForProfit(proletariat) {
    const labor = proletariat.employ()
    const goods = this.capital.produce(labor)
    return goods.sellTo(proletariat) + this.land.rentTo(proletariat)
  }
}
// Usage
function initClassStruggle() {
  const land = Earth.getLand()
  const capital = Society.industrialize()
  const bourgeoisie = new Bourgeoisie(land, capital)
  Society.on('workday', () => {
    bourgeoisie.exploitForProfit(Society.getProletariat())
  })
}
initClassStruggle()

클래스의 방식은 constructor로 생성될 객체의 변수들을 지정해주고, 메소드를 넣어준다.

실제 사용시 new Operator로 해당 클래스 프로토타입에서 형태를 상속시킨 객체를 만들어주고 메소드 사용시 obj.method 이르케 객체에서 메소드를 호출한다.

2-2. 클로저 방식

const Earth = require('earth')
const Society = require('society')
// A closure.
function Bourgeoisie(land, capital) {
  function exploitForProfit(proletariat) {
    const labor = proletariat.employ()
    const goods = capital.produce(labor)
    return goods.sellTo(proletariat) + land.rentTo(proletariat)
  }
  return {
    exploitForProfit: exploitForProfit,
  }
}
// Usage
function initClassStruggle() {
  const land = Earth.getLand()
  const capital = Society.industrialize()
  const bourgeoisie = Bourgeoisie(land, capital)
  Society.on('workday', () => {
    bourgeoisie.exploitForProfit(Society.getProletariat())
  })
}

클로저 방식은 변수에 함수의 리턴값을 넣고 리턴값은 해당 함수 안의 변수들을 참조하는 방식이다.

즉, 클래스를 통해 객체를 생성해서 메소드를 통해 함수를 실행하는 방식이 아니라 리턴값을 갖는 함수의 특성과 실행 컨텍스트의 scope chain, 변수를 유지하는 특성을 이용한 방식이 클로저 패턴이다.

🐥 클로저로 상태관리 하려면 변수에 함수를 넣어주는 것이 핵심이다!!

3. 클로저 간단 원리

클로저가 뭔지 알겠다. 근데 이게 어떻게 발생하는 것일까?

자바스크립트는 실행가능한 객체 함수를 만나면 혹은 전역객체 환경일 때, scope chain을 구성하고 변수들을 저장한다. 그리고 이 과정을 💡lexical scope 방식으로 한다.

2-1. 클로저 x

function Add() {
  var a = 1
  return function() {
    a += 1
    return a
  }
}
Add()() // 2
Add()() // 2

위 코드에서는 함수를 호출하고 리턴값의 함수를 다시 호출한다.

변수 a 는 Add() 일케 호출 할 때마다 1로 초기화되며 Add()() 하면 그 a 를 읽어서 항상 2를 출력한다.

2-2. 클로저 o

function Add() {
  var a = 1
  return function() {
    a += 1
    return a
  }
}
var closure = Add()
closure() // 2
closure() // 3

하지만 Add()함수를 호출한 값을 변수에 담고 그 변수안에서 호출한 값을 실행시키면 a는 기억되며 숫자가 정상적으로 올라간다.

console.log(Add())
console.log(closure)
/* 출력값
ƒ () {
    a += 1
    return a
  }
*/

이렇게 closure를 찍어보면 a에 1을 더하고 그 값을 리턴하는 함수만 존재한다.

클로저는 이렇게 실행 컨텍스트의 lexical scope를 기억한다.

2-3. 클로저 다시생각해보기!

실행 컨텍스트에서 실행가능한 함수 객체를 만나면 내부 정보(변수나 scope chain)을 기억하는데, 변수에 함수를 실행한 값을 저장하면 그 변수의 실행컨텍스트 환경에서 내부 정보들이 기억된다. 그리고 리턴값이 함수여서 해당 변수를 실행시켜 내부 리턴값의 함수를 실행하면 이전의 기억된 정보들이 유지된 채로 남아있다.

3. hooks

“With hooks, beginners no longer need to learn about ‘this’ to avoid shooting themselves in the foot.”

Mark Dalgleish

4. 리액트 컴포넌트 (OOP)

예제 출처: 누구든지 하는 리액트 4편: props 와 state - velopert

import React, { Component } from 'react'

class Counter extends Component {
  state = {
    number: 0,
  }

  handleIncrease = () => {
    this.setState({
      number: this.state.number + 1,
    })
  }

  render() {
    return (
      <div>
        <h1>카운터</h1>
        <div>값: {this.state.number}</div>
        <button onClick={this.handleIncrease}>+</button>
      </div>
    )
  }
}

export default Counter

클래스 형식의 컴포넌트를 만들고 해당 클래스의 멤버변수 state를 둔다.

state의 속성값을 변경하려면 컴포넌트 내장 메소드 setState를 상속받은 해당 객체가 setState메소드를 사용해서 상태를 변경하고 콜백을 통해 해당 DOM을 리렌더링한다.

4-1. setState와 불변성

1. 리액트의 단방향 바인딩

정보가 업데이트 되면 virtual DOM 에 렌더링 하고 실제 DOM 과 비교해서 업데이트 된부분을 수정하는 단방향 바인딩의 특성 때문에 불변성을 유지해주어야한다.

2. 불변성

리액트가 정보가 업데이트 됐는지 확인하려면 객체를 직접수정해서는 알 수 없으니 (참조타입의 특징) setState 메소드를 통해 상태변경의 유무를 확인한다.

3. 불변성을 유지하는 방법1

원시타입일 경우, 그냥적어준다.

4. 불변성을 유지하는 방법2

배열, 객체일 경우, …spread 문법으로 펴주기 (객체리터럴은 ES2018에서 추가)

5. 불변성을 유지하는 방법3

배열일 경우, concat()

6. immer

불변성은 코드가 길어지고 직관적이지 않기 때문에 immer나 immutable 같은 라이브러리도 존재한다.

5. 리액트 컴포넌트 (functional + closure => hooks)

🐥 왜 함수형 컴포넌트 state, life cycle안되는지 : 클래스형은 인스턴스 객체형태라 그 안의 state 사용 + render() , state변경-> render // 함수형은 실행

5-1. hooks

import React, { useState } from 'react'

// ⭐ 함수선언형
function Comp() {
  // 🍎 hooks
  const [count, setCount] = useState(0)

  // ⭐ 함수표현식
  // const Comp = function() {
  //   const [count, setCount] = useState(0)

  // ⭐ 화살표함수
  // const Comp = () => {
  //   const [count, setCount] = useState(0)

  // ⭐ 외부함수사용
  // const clickHandler = () => {
  //   setCount(count+1)
  // }

  // ⭐ no render()
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

export default Comp
🐥 라이프사이클공부

5-2. useState

function useState(initialValue) {
  let _val = initialValue
  // 함수
  const state = () => _val
  // 함수
  const setState = newVal => {
    _val = newVal
  }

  // 리턴
  return [state, setState]
}

const [count, setCount] = useState(1)
console.log(count()) // 1

setCount(2)
console.log(count()) // 2

5-3. useEffect

import React, { useEffect } from 'react'

useEffect(() => {
  alert('마운팅 완료!')
}, [])
// 주요특징은 state에 접근이 가능
🐥 useEffect 뜯어보기

5-4. useCallback

import React, { useCallback } from 'react'

const memoizedCallback = useCallback(() => {
  doSomething(a, b)
}, [a, b])
// a,b의 값의 변화가 감지될 때야 리렌더링
🐥 useCallback 뜯어보기

5-5. useRef

function TextInputWithFocusButton() {
  const inputEl = useRef(null)
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus()
  }
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  )
}

Written by@taenyKim
웹 프론트엔드 공부 블로그 / Learn in Public

GitHubFacebook