2. 리액트 개념 이해하기

조은·2021년 9월 9일

React Programming Study

목록 보기
1/4

2.1 리액트를 사용한 코드의 특징

  • 리액트를 사용하지 않은 코드

    <html>
        <body>
            <div class="todo">
                <h3>할 일 목록</h3>
                <ul class="list"></ul>
                <input class="desc" type="text" />
                <button onclick="onAdd()">추가</button>
                <button onclick="onSaveToServer()">서버에 저장</button>
            </div>
            <script>
                let currentId = 1;
                const todoList = [];
                function onAdd(){
                    const descEl = document.querySelector('.todo .desc');
                    const todo = { id: currentId++, desc: descEl.value };
                    todoList.push(todo);
                    const listEl = document.querySelector('.todo .list');
                    const todoEl = makeTodoElement(todo);
                    listEl.appendChild(todoEl);
                }
                function makeTodoElement(todo) {
                    const todoEl = document.createElement('li');
                    const spanEl = document.createElement('span');
                    const buttonEl = document.createElement('button');
                    spanEl.innerHTML = todo.desc;
                    buttonEl.innerHTML = '삭제';
                    buttonEl.dataset.id = todo.id;
                    buttonEl.onclick = onDelete;
                    todoEl.appendChild(spanEl);
                    todoEl.appendChild(buttonEl);
                    return todoEl;
                }
                function onDelete(e) {
                    const id = Number(e.target.dataset.id);
                    const index = todoList.findIndex(item => item.id === id);
                    if (index >= 0) {
                        todoList.splice(index, 1);
                        const listEl = document.querySelector('.todo .list');
                        const todoEl = e.target.parentNode;
                        listEl.removeChild(todoEl); 
                    }
                }
                function onSaveToServer() {
                    //todoList를 서버로 전송한다.
                }
            </script>
        </body>
    </html>
  • 리액트를 사용한 코드

    import React, { useState } from 'react';
    
    export default function App() {
      const [todoList, setTodoList] = useState([]);
      const [currentId, setCurrentId] = useState(1);
      const [desc, setDesc] = useState('');
    
      function onAdd() {
        const todo = { id: currentId, desc };
        setCurrentId(currentId + 1);
        setTodoList([...todoList, todo]);
      }
      function onDelete(e) {
        const id = Number(e.target.dataset.id);
        const newTodoList = todoList.filter(todo => todo.id !== id);
        setTodoList(newTodoList);
      }
      function onSaveToServer() {}
      return (
        <div>
          <h3>할 일 목록</h3>
          <ul>
            {todoList.map(todo => (
              <li key={todo.id}>
                <span>{todo.desc}</span>
                <button data-id={todo.id} onClick={onDelete}>
                  삭제
                </button>
              </li>
            ))}
          </ul>
          <input type='text' value={desc} onChange={e => setDesc(e.target.value)} />
          <button onClick={onAdd}>추가</button>
          <button onClick={onSaveToServer}>서버에 저장</button>
        </div>
      )
    }

리액트를 사용하지 않은 코드

  1. 이벤트 핸들러에 데이터를 변경하는 작업과 UI변경 코드도 포함
  2. 비즈니스 로직과 UI코드가 복잡하게 혼합되어 작성
  3. 명령형 프로그래밍
  4. UI의 변화된 모습을 예상하기 어려움
  5. 돔 환경에만 적합
  6. 추상화 단계 낮음

리액트를 사용한 코드

  1. 이벤트 핸들러에 데이터를 변경하는 작업만 실행
  2. 비즈니스 로직과 UI코드 분리하여 작성
  3. 선언형 프로그래밍
  4. UI의 변화를 직관적으로 예상 가능
  5. 다양한 방식으로 코딩 가능
  6. 추상화 단계 높음
    ⇒ 비즈니스 로직에 집중 가능

리액트로 코드를 작성 시 UI코드는 선언형으로 작성, 이벤트 핸들러에서는 데이터만 수정하게 되면 리액트가 자동으로 UI를 렌더링 해준다

2.2 컴포넌트의 속성값과 상태값

아래 코드는 UI데이터를 속성값이나 상태값으로 관리하지 않은 코드이다. (외부에 변수를 만들어 놓고 쓰는 형태)

import React from 'react';

let color = 'red';
export default function App() {
  function onClick() {
    color = 'blue'; //red에서 blue로 변경함
  }
  return (
    <button style={ { backgroundColor: color } } onClick={onClick}>
      좋아요
    </button>
  );
}

이 코드의 문제점은 좋아요 버튼을 눌러도 빨간색 버튼이 파란색으로 변하지 않는다는 것이다.

컬러값은 변경했지만 리액트가 값이 변경됐다는 사실을 모른다.

따라서, UI 데이터는 상태값 또는 속성값으로 관리해야함

(1) 상태값

import React, { useState } from 'react';

export default function App() {
  const [color, setColor] = useState('red');
  function onClick() {
    setColor('blue'); // 상태값을 변경해줌
  }
  return (
    <button style={ { backgroundColor: color } } onClick={onClick}>
      좋아요
    </button>
  );
}

color 변수는 setColor를 통해 상태값이 바뀐다.

이제 리액트가 속성값 변경을 인지하고 버튼 클릭 시 빨간색에서 파란색으로 변경 됨

즉, 상태값은 상태가 변경될 때마다 값이 바뀌는 변수다.

(2) 속성값

import React, { useState } from 'react';
import Title from './Title';

export default function Counter() {
    const [count, setCount] = useState(0);
    function onClick() {
        setCount(count + 1);
    }
    return (
        <div>
            <Title **title={ `현재 카운트: ${count}` }** />
            <button onClick={ onClick }>증가</button>
        </div>
    );
}
import React from 'react';

//props: 부모 컴포넌트가 전달해주는 속성값
export default function Title({ title }) { //객체 비구조화 문법을 통해 .props 생략
    return <p>{ title }</p>;
}

부모 컴포넌트에서 Title이라는 컴포넌트 사용, 자식에게 props.title이라는 속성값을 내려주고 있다.

  1. count라는 상태값을 기반으로 title값을 계산
  2. count값이 변경되면 Counter(부모)컴포넌트는 다시 렌더링, Title(자식)컴포넌트도 다시 렌더링
  3. 부모 컴포넌트가 렌더링 될때마다 자식 컴포넌트도 같이 렌더링 됨

1) 속성값 특징 - React.memo

자식의 속성값이 변경되지 않았을 때 굳이 렌더링이 필요하지 않은 경우

import React, { useState } from 'react';
import Title from './Title';

export default function Counter() {
    const [count, setCount] = useState(0);
    **const [count2, setCount2] = useState(0);**
    function onClick() {
        setCount(count + 1);
    }
    **function onClick2() {
        setCount2(count2 + 1);
    }**
    return (
        <div>
            <Title title={ `현재 카운트: ${count}` } /> 
            <button onClick={ onClick }>증가</button>
            **<button onClick={ onClick2 }>증가2</button>**
        </div>
    );
}

증가2 버튼을 눌렀을 때 title 속성값이 변경되지 않았음에도 Title(자식)컴포넌트가 렌더링 됨

속성값이 변경될때만 자식 컴포넌트가 렌더링 되게 할 수 있음 ⇒ React.memo사용

import React from 'react';

function Title({ title }) {
    console.log('타이들 렌더링 됨')
    return <p>{ title }</p>;
}

export default **React.memo(Title)**;

React.memo사용을 통해 속성값 title이 변경될 때만 이 컴포넌트가 렌더링 됨

2) 속성값 특징 - 컴포넌트 중복 사용

같은 컴포넌트를 여러번 사용할 수도 있음

import React from 'react';
import Counter from './Counter';

export default function App() {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  );
}

화면 캡처 2021-08-30 181941.png

위 코드에서 사용된 각 Counter컴포넌트는 상태값을 위한 자신만의 메모리 공간이 있어서 같은 컴포넌트여도 자신만의 상태값이 존재한다.

두 카운트 값은 각자 유지됨

3) 속성값 특징 - 상태값, 속성값 불변변수로 관리

속성 값은 불변 변수이지만 상태값은 불변변수가 아님 하지만 상태값도 불변 변수로 관리하는게 좋음

  • 속성값

    import React from 'react';
    
    function Title(props) {
        **props.title = 'asdf';**
        return <p>{ props.title }</p>;
    }
    
    export default React.memo(Title);

    화면 캡처 2021-08-30 174237.png

    속성값은 불변변수이다.

    속성값은 불변변수이기때문에 값을 변경하려고 하면 에러가 발생함

    자식 컴포넌트에 전달되는 속성값은 상위 컴포넌트에서 관리하기 때문에 수정하지 못하도록 막혀있음

    title값을 수정하고 싶다면 부모컴포넌트에서 상태값 변경 함수를 이용해야함

  • 상태값

    import React, { useState } from 'react';
    import Title from './Title';
    
    export default function Counter() {
        const [count, setCount] = useState({ value: 0 });
        function onClick() {
            count.value += 1; **//값을 직접 수정하고 있음**
            setCount(count);
        }
        return (
            <div>
                <Title title={ `현재 카운트: ${count.value}` } /> 
                <button onClick={ onClick }>증가</button>
            </div>
        );
    }

    버튼을 클릭해도 count값이 변경되지 않음

    내부 속성값은 바뀌었지만 객체인 count의 참조값은 변경되지 않았기 때문에 리액트는 변화가 없다고 인식한다.

    리액트는 상태값 변경 유무를 이전 값과의 단순 비교로 판단하는데 count 참조 값이 변하지 않았기에 무시되는 것.

    위 2.2.(1)에서 상태값은 값이 변하는 변수라고 했지만 불변변수로 관리할 수 있다. (불변변수로 관리하는 것이 좋다.)
    이유는? 불변변수로 관리하면 코드의 복잡도도 낮아지는 장점이 있기때문.

    상태값을 불변변수로 관리하는 한 가지 방법: 전개 연산자를 이용하는 것

    import React, { useState } from 'react';
    import Title from './Title';
    
    export default function Counter() {
        const [count, setCount] = useState({ value: 0 });
        function onClick() {
            setCount({ **...count**, value: count.value + 1 });
        }
        return (
            <div>
                <Title title={ `현재 카운트: ${count.value}` } /> 
                <button onClick={ onClick }>증가</button>
            </div>
        );
    }

2.3 컴포넌트 함수의 반환값

  1. 리액트 요소

    export default function App() {
    	return <div>안녕하세요</div>;
    }
  2. 컴포넌트

    export default function App() {
    	return <Counter />;
    }
  3. 문자열

    export default function App() {
    	return '안녕하세요';
    }
  4. 숫자

    export default function App() {
    	return 123;
    }
  5. 배열

    export default function App() {
    	return ['안녕','하세요']
    }
  6. 배열 속 요소

    export default function App() {
    	return [<p>안녕</p>,<p>하세요</p>]
    }

    화면 캡처 2021-08-31 134203.png

    "key" prop이 없다는 에러가 뜬다.

    • 배열은 key값이 필요

      export default function App() {
      	return [<p key={1}>안녕</p>,<p key={2}>하세요</p>]
      }

      배열로 반환할 때는 항상 리액트 요소가 key를 가지고 있어야 함

      key는 렌더링을 효율적으로 하기 위해서 필요한 값

      리액트가 이 값을 이용해서 virtual dom(가상 돔)에서의 연산을 효율적으로 할 수 있다.

  7. Fragment

    export default function App() {
    	return (
    		<React.Fragment>
    			<p>안녕</p>
    			<p>하세요</p>
    			{null}
    			{false}
    			{true}
    		</React.Fragment>
    	)
    }

    리액트는 문법상 모든 코드를 감싸는 태그가 있어야한다.

    이 때문에 원치않아도 div를 사용해서 요소를 감싸야할 때가 있는데, 이를 보완하기 위해 비교적 최근에 나온것이 <React.Fragment>이다.

    이것은 실제 돔에는 반영이 되지 않아 렌더링되지 않는다.

    Fragment안에 요소는 순서가 key의 역할을 하기 때문에 key를입력하지 않아도 에러가 나지 않는다.

    Fragment 안에서는 null, boolean 값도 반환 가능하지만 브라우저에 출력은 되지 않음

    • <React.Fragment></React.Fragment>의 축약형 : <> </>
    {count.value && <Title title={ `현재 카운트: ${count}` } /> }

    boolean은 조건부 렌더링을 할 때 유용하게 사용 됨.

    count.value가 true이면 Title컴포넌트가 출력, false이면 출력되지 않는다.

  8. 리액트 Potal

    <div id="root"></div>
    <div id="something"></div>
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    export default function App() {
    	return (
    		<>
          <p>안녕</p>
          {ReactDOM.createPortal(
              <div>
                <p>안녕하세요</p>
                <p>실전 리액트 프로그래밍입니다.</p>
              </div>,
              document.getElementById('something'),
          )}
    		</>
    	)
    }

    리액트 Potal을 사용하기 위해서는 react-dom에 있는 함수를 사용

    리액트 Potal은 보통 모달(Modal)을 위해서 많이 사용

2.4 리액트 요소와 가상돔 1

(1) Virtual DOM(가상돔) 사용 이유

HTML 마크업을 시각적인 형태로 변환하는 것은 웹브라우저가 하는 주 역할이기 때문에 이를 처리할 때 컴퓨터 자원을 사용하는 것은 어쩔 수 없다. DOM은 트리 구조로써, 업데이트 후에 변경된 요소와 하위 요소를 다시 렌더링 하여 웹브라우저 UI를 업데이트해야 한다. 따라서 UI 구성 요소가 많을수록 DOM 트리를 다시 렌더링 해야 되므로 성능이 저하된다.
이런 문제를 해결하려면 DOM을 최소한으로 조작하여 작업해야 된다.
리액트는 Virtual DOM 방식을 사용하여 DOM 업데이트를 추상화함으로써 DOM 처리 횟수를 최소화하고 효율적으로 진행한다.

(2) Virtual DOM 작동 방식

  1. 데이터를 업데이트하면 전체 UI를 Virtual DOM에 리렌더링한다.
  2. 리액트는 메모리에 가상돔을 올려놓고 이전과 이후의 가상돔을 비교한다.
  3. 바뀐 부분만 실제 DOM에 적용한다.

(3)리액트 요소의 형식

const element = (
    <a key="key1" style={{ width: 100 }} href="http://google.com">
        click here
    </a>
);

console.log(element);

**const consoleLogResult = {
    type: 'a',
    key: 'key1',
    ref: null,
    props: {
        href: "http://google.com",
        style: {
            with: 100,
        },
        children: 'click here',
    },**
};

리액트 요소는 객체 형식으로 이루어져 있음

function Title({ title, color }) {
    return <p style={{ color }}>{ title }</p>;
}

const element = <Title title="안녕하세요" color="blue" />;

console.log(element);

**const consoleLogResult = {
    type: Title,
    props: { title: '안녕하세요', color: 'blue' },
};**

컴포넌트를 이용해서 리액트 요소를 만드는 경우에는 type에 컴포넌트가 입력되어 있음

const element = <a href="http://google.com">cilck here</a>;

element.type = 'b'; **//에러 발생**

리액트 요소는 불변객체이기 때문에 변경하려고하면 에러 발생한다. (리액트 요소는 변경할 수 없음)

(4)컴포넌트 Lifecycle (mount, unmount)

<Counter key={seconds} />
//여기서 seconds의 초기값은 0이며, setSeconds를 이용해 seconds는 1초마다 변경되는 변수임.

컴포넌트의 key를 변경 시 해당 컴포넌트는 삭제됐다가 다시 추가가 된다.

따라서, Counter 컴포넌트는 1초마다 삭제되었다가 다시 생성되어 추가된다.

여기서 컴포넌트가 삭제되는 것을 unmount, 컴포넌트가 추가되는 것을 mount라고 부름

컴포넌트가 추가될 때는 useState에 첫번째 매개변수로 입력된 초기값이 상태값으로 할당된다. 따라서 위 코드에선 1초에 한번씩 0이 할당됨. 이렇게 key를 변경하면 mount와 unmount가 반복됨.

{ seconds % 2 === 0 && <Counter /> }
//여기서 seconds는 1초마다 변경되는 변수임.

조건부 렌더링으로 위의 코드와 비슷한 효과를 볼 수가 있음

위 코드는 seconds 변수가 짝수일 때만 Counter컴포넌트가 추가되어 생성되면서 mount, unmount가 반복됨.

2.5 리액트 요소와 가상돔 2

(1) 리액트 요소 트리

하나의 화면을 표현하기 위해서 여러 개의 리액트 요소가 트리 구조로 구성이 된다.

jsx구조

<div>
	<p>안녕하세요</p>
	<div>	
		<p>나이: 23</p>
		<p>나이: 23</p>
	</div>
</div>

(2) 렌더 단계와 커밋 단계

리액트에서 데이터 변경에 의한 화면 업데이트는 랜더 단계와 커밋 단계를 거친다.

렌더 단계

실제돔에 반영할 변경사항을 파악하는 단계

  • 렌더 단계에서는 변경 사항을 파악하기 위해서 가상 돔을 이용한다. 가상돔은 리액트 요소로부터 만들어지고
    리액트는 렌더링 할 때마다 가상돔을 만들고 이전의 가상 돔과 비교한다.
  • 이는 실제 돔의 변경 사항을 최소화하기 위한 과정이다.
  • 렌더 단계는 랜덤 함수를 호출하거나 또는 컴포넌트 내부에서 상태값 변경 함수를 호출해서 시작될 수 있다.

정리!
1. 렌더 함수가 호출되면서 최초의 렌더 단계가 실행됨 이렇게 만들어진 가상돔이 실제 돔으로 만들어짐
2. 사용자의 버튼 클릭으로 Todo컴포넌트의 상태값이 변경됨.
3. 곧 두번째 렌더단계가 실행이 되고 새로운 가상돔이 만들어짐 (이때 이전의 가상돔과 비교해서 변경된 부분만 실제 돔에 반영됨)

2.6 리액트 훅 기초 익히기 1

(1) 리액트 훅(hook)

  • 컴포넌트에 기능을 추가할 때 사용하는 함수

    ex)컴포넌트에 상태값 추가, 자식 요소에 접근

    리액트 16.8에 새로 추가 됨

    그 전에는 클래형 컴포넌트 사용

    클래스형 컴포넌트보다 장점이 많으며 리액트 팀에서도 훅에 집중하고 있음

  • 리액트에서 제공하는 대표적인 두 가지 훅

    1. useState: 상태값 추가

    2. useEffect: 부수효과(외부의 상태를 변경하는 것) 처리

      서버 API 호출, 이벤트 핸들러 등록 등

(2) useState의 사용법

useState는 함수형 컴포넌트에서 상태값을 관리하게 해주며 [상태값, 상태값 변경 함수]인 배열을 반환한다.

1) 상태값 변경 함수는 비동기이면서 배치(batch)로 처리

import React, { useState, useEffect } from 'react';

export default function App() {
    const [count, setCount] = useState(0);
    function onClick() {
        **setCount(count + 1);**
        **setCount(count + 1);**
    }
    console.log('render called');
    return (
        <div>
            <h1>{ count }</h1>
            <button onClick={ onClick }>증가</button>
        </div>
    );
}
  1. useState로 count에 초기값 0을 넣어서 호출
  2. setCount를 2번 호출해 count 값 변경
  3. 하지만 코드처럼 count 값이 2번씩 커지지 않는다

이유? 상태값 변경 함수는 비동기이면서 배치(batch)로 처리되기 때문에 이 코드가 의도한대로 동작하지 않는 것.

  • 만약 리액트가 상태값 변경 함수를 동기로 처리하면 하나의 상태값 변경 함수가 호출될 때마다 화면을 다시 그리기 때문에 성능 이슈가 생길 수 있음

  • 만약 상태값 변경은 동기로 처리하고 매번 화면을 다시 그리지 않는다면 UI 데이터와 화면강의 불일치가 발생해서 문제가 생김

import React, { useState, useEffect } from 'react';

export default function App() {
    const [count, setCount] = useState(**{ value: 0 }**);
    function onClick() {
        **setCount({ value: count.value + 1 });**
        **setCount({ value: count.value + 1 });**
    }
    console.log('render called');
    return (
        <div>
            <h1>{ count }</h1>
            <button onClick={ onClick }>증가</button>
        </div>
    );
}

상태값이 객체이더라도 제대로 동작하지 않음

2) 하나의 함수에서 같은 상태값 변경 함수 2번 적용하기

import React, { useState, useEffect } from 'react';

export default function App() {
    const [count, setCount] = useState(0);
    function onClick() {
        **setCount(v => v + 1);**
        **setCount(v => v + 1);**
    }
    console.log('render called');
    return (
        <div>
            <h1>{ count }</h1>
            <button **onClick={ onClick }**>증가</button>
        </div>
    );
}

제대로 동작하게 하려면 상태값 변경 함수에 함수를 입력하면 된다.

이렇게하면 처리되기 직전의 상태값을 매개변수로 받기 때문이다.

여기서 onClick 이벤트 핸들러는 리액트 내부에서 관리되는
리액트 요소에 입력(
<button **onClick={ onClick }**>증가</button>)되어있기때문에 배치로 처리된 것,
이렇게 하지않고 리액트에서 관리하지 않는 외부에서 호출하는 경우에는 배치로 동작하지않음

즉, 그러한 경우에는 상태값 변경 함수는 호출할 때마다 렌더링이 발생

3) 리액트 외부 함수를 배치로 처리하기

외부에서 배치로 처리되지 않는 코드

import React, { useState, useEffect } from 'react';

export default function App() {
    const [count, setCount] = useState(0);
    function onClick() {
        setCount(v => v + 1);
        setCount(v => v + 1);
    }
    **useEffect(() => {
        window.addEventListener('click', onClick);
        return () => window.removeEventListener('click', onClick);
    });**
    console.log('render called');
    return (
        <div>
            <h1>{ count }</h1>
            <button onClick={ onClick }>증가</button>
        </div>
    );
}

위 코드처럼 이벤트 핸들러를 등록해서 사용하는 경우에 리액트 내부에서 관리되지 않기 때문에 배치로 처리되지 않는다.

따라서, cosole에 render called이 두번 찍힌다.

외부에서 배치로 처리되는 코드

import React, { useState, useEffect } from 'react';

export default function App() {
    const [count, setCount] = useState(0);
    function onClick() {
        ReactDOM.**unstable_batchedUpdates**(() => {
            setCount(v => v + 1);
            setCount(v => v + 1);
        });
    **}**
		useEffect(() => {
        window.addEventListener('click', onClick);
        return () => window.removeEventListener('click', onClick);
    });
    console.log('render called');
    return (
        <div>
            <h1>{ count }</h1>
            <button onClick={ onClick }>증가</button>
        </div>
    );
}

cosole에 render called이 한번만 찍힌다. ⇒ 배치로 처리되고 있다.

만약 외부에서 처리될 때도 배치로 처리되길 원한다면 batchedUpdates라는 함수를 호출하면됨

cf) concurrent mode로 동작할 미래의 리액트는 외부에서 관리되는 이벤트 처리 함수도 배치로 처리될 예정

4) 상태값 변경 함수 호출 순서

import React, { useState } from 'react';

export default function App() {
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);
    function onClick() {
        setCount1(count1 + 1)
        setCount2(count2 + 1)
    }
    const result = count1 >= count2;
    // ...
}

상태값 변경 함수는 호출한 순서대로 적용이 되기 때문에 여기서는 count1, count2 순으로 적용이 된다.

  • 배치로 적용될 경우: 이런 순서 정보가 딱히 의미가 없다
  • 배치가 아닌 경우: 한 번 호출할 때마다 렌더링이 될텐데
    그때 이렇게 컴포넌트 함수에서 const result = count1 >= count2 ⇒ 이런 수식을 작성했다면 이 값은 항상 true가 됨

5) 하나의 useState로 여러개의 상태값을 관리(객체)

import React, { useState } from 'react';

export default function App() {
    const [state, setState] = useState({ name: '', age: 0 });
    return (
        <div>
            <p>{`name is ${state.name}`}</p>
            <p>{`age is ${state.age}`}</p>
            <input 
                type='text'
                value={state.name}
                onChange={e => setState({ ...state, name: e.target.value })}
            />
            <input 
                type='number'
                value={state.age}
                onChange={e => setState({ ...state, age: e.target.value })}
            />
        </div>
    );
    
}

useState는 객체 형태의 상태값도 관리할 수 있다.

주의할 점은 함수형 컴포넌트에서는 이전 상태값을 지우기 때문에 상태값 변경 함수를 호출할 때는 전체 객체를 새로 입력 해줘야 한다.

앞에는 ...state를 작성, 변경할 값은 뒤에 작성하여야 한다.

cf) 사실 이렇게 여러 개의 상태값을 관리할 때는 useState보다는 useReducer라는 훅이 더 적합함

(3) useEffect의 사용법

특별한 이유가 없다면 모든 부수효과는 useEffect훅에서 처리하는 것이 좋음

컴포넌트 렌더링 중에 부수 효과를 발생시키는 것은 프로그램의 복잡도를 크게 증가시키는데 유닛테스트를 작성하기 힘들어지는 등 순수 함수가 가지는 여러 장점을 포기하는 것이다.

컴포넌트 렌더링 중에 부수 효과를 발생시킬 이유가 있을까? 대부분 렌더링 후에 처리해도 된다.

게다가 concurrent mode로 동작할 미래의 리액트는 컴포넌트 함수가 호출되도 중간에 렌더링이 취소될 수 있다.

즉, 한번 렌더링 하기 위해 컴포넌트 함수가 여러 번 호출될 수도 있다는 것이다.

그러니까 컴포넌트 함수 내부에서 직접 실행하는 부수 효과가 있다면 한번 렌더링할 때 여러 번 부수 효과가 실행 될 수도 있다는 것이다.

1) useEffect의 첫번째 매개 변수 - 부수 효과 함수

	import React, { useState, useEffect } from 'react';

export default function App() {
    const [count, setCount] = useState(0);
    useEffect(**() => {
        document.title = `업데이트 횟수: ${count}`;
    }**);
    return <button onClick={ () => setCount(count + 1) }>증가</button>
    
}

useEffect에 첫번째 매개 변수로 입력한 함수는 부수 효과 함수이라고 부르며

  1. 컴포넌트가 렌더링된 후에

  2. 비동기로 호출된다.

<button>을 클릭할 때마다 컴포넌트는 다시 렌더링된 뒤, useEffect도 실행된다.

2.7 리액트 훅 기초 익히기 2

2) useEffect의 두번째 매개 변수 - 의존성 배열

import React, { useState, useEffect } from 'react';

export default function Profile({ userId }) { 
    const [user, setUser] = useState(null);
    useEffect(() => {
        getUserApi(userId).then(data => setUser(data));
    }, **[userid]**); //의존성 배열!!
    return ( 
        <div>
            {!user && <p>사용자 정보를 가져오는 중...</p>}
            {user && (
                <>
                    <p>{`name is ${user.name}`}</p>
                    <p>{`age is ${user.age} `}</p>
                </>
            )}
        </div> 
    );

const USER1 = { name: 'mike', age: 23 }; 
const USER2 = { name: 'jane', age: 31 }; 
function getUserApi(userId) { 
    return new Promise(res => { 
        setTimeout(() => {
            res(userId % 2 ? USER1 : USER2);
        }, 500);
    });
}

만약 위 코드에서 렌더링이 자주 발생한다면 그때마다 api를 호출하는 것은 비효율적이다.

이럴 때는 두번째 매개 변수에 배열을 입력하는데 이것을 의존성 배열이라고 부른다.

  1. 의존성 배열이 변경될 때만 부수 효과 함수가 실행되어 불필요한 api호출 빈도를 낮춘다.
  2. 만약 의존성 배열에 빈 배열을 입력하면 부수 효과 함수는 마운트된 이후에 한번만 호출된다.

3) 의존성 배열에 입력하는 값

의존성 배열에는 부수 효과 함수에서 사용한 변수를 잘봐야한다.

컴포넌트의 1. 상태값이나 2. 속성값 또는 이 3. 컴포넌트 내부에서 정의된 지역 변수나 지역 함수들은 모두 의존성 배열에 작성을 해줘야한다.

위 코드를 살펴보면

  1. getUserApi는 외부에 있는 함수이기 때문에 의존성 배열을 입력을 하지 않아도 된다.

  2. userId는 속성값이기 때문에 입력해줘야 한다.

  3. setUser 상태값 변경 함수는 값이 변경되지 않는다는 것이 보장되기 때문에 상태값 변경 함수는 예외적으로 의존성 배열에 입력하지 않아도 괜찮다.

  4. 지역변수

    //앞부분 생략
    
    export default function Profile({ userId }) { 
        const [user, setUser] = useState(null);
    		**const value = userId + 10;**
        useEffect(() => {
    				console.log(value);
            getUserApi(userId).then(data => setUser(data));
        }, **[userid, value]**);
    
    //뒷부분 생략

    지역 변수(value)를 사용한다고 하면 반드시 이 지역 변수도 의존성 배열에 입력해줘야 한다.

  5. 지역 함수

    //앞부분 생략
    
    export default function Profile({ userId }) { 
        const [user, setUser] = useState(null);
    		**function func1() {
    			console.log(userId);
    		}**
        useEffect(() => {
    				**func1();**
            getUserApi(userId).then(data => setUser(data));
        }, **[userid, func1]**);
    
    //뒷부분 생략

    지역함수(func1)를 부수 효과 함수 내부에서 사용했다면 의존성 배열에 입력해줘야 한다.

    그런데 이렇게 입력해줘도 cmd창에 func1 함수가 너무 자주 변경되어 문제가 될 수있다고 경고가 뜬다.

    Profile 컴포넌트가 렌더링될 때마다 func1 함수가 새로 생성되고 있기 때문에 사실상 이 의존성 배열의 값이 항상 변경되기 때문!

    이럴 때는 useCallback이라는 훅을 이용해서 메모이제이션 기능을 이용하는 방법이 있다. ⇒ useCallback은 나중에

cf) 의존성 배열은 꼭 필요한 경우에만 입력하는게 좋다

새로운 상태값을 추가한 다음에 부수 효과 함수에서 사용을 하면 매번 사용한 변수를 의존성 배열에 추가해야하는 번거로움이 생기는데
useEffect훅을 사용할 때 많은 버그가 의존성 배열을 잘못입력한 것에서 나온다.

4) 부수효과함수가 반환하는 값

import React, { useState, useEffect } from 'react';

export default function WidthPrinter() {
    const [width, setWidth] = useState(window. innerWidth);
    useEffect(() => { 
        const onResize = () => setWidth(window.innerWidth);
        window.addEventListener('resize', onResize); 
        **return () => { 
            window.removeEventListener('resize', onResize);
        };**
    }, []);
			//부수 효과 함수에서 새로운 함수를 또 반환하고 있음
    return <div>{ `width is ${width}` }</div>;
	};

부수 효과 함수에서 return뒤에 있는 함수 반환하는 이 함수는

  1. 다음 부수 효과 함수가 호출되기 직전에 호출
  2. 컴포넌트가 사라지기 직전에(즉, unmount)되기 직전에 마지막으로 호출

따라서 부수 효과 함수가 반환한 함수는 컴포넌트가 unmount되기 전에 적어도 한번은 호출 된다는 것이 보장된다.

의존성 배열로 빈 배열을 입력하면

  1. 컴포넌트가 생성될 때만 부수 효과 함수가 호출되고
  2. 컴포넌트가 사라질 때만 이 반환한 함수가 호출 된다.

그래서 아래 코드처럼 이벤트 핸들러를 등록하고 해제하는 패턴의 함수를 작성할 수 있다.

import React, { useState } from 'react'; 
import WidthPrinter from './WidthPrinter';

export default function App() {
const [count, setCount] = useState(0); 
    return(
        <>
            { count === 0 && <WidthPrinter/> }
            <button onClick={() => setCount(count + 1) }>증가</button>
        </>
    );
}
import React, { useState, useEffect } from 'react';

export default function WidthPrinter() {
    const [width, setWidth] = useState(window. innerWidth);
    useEffect(() => {
        const onResize = () => setWidth(window.innerWidth);
        window.addEventListener('resize', onResize);
				**console.log('useEffect 1');**
        return () => {
            window.removeEventListener('resize', onResize);
						**console.log('useEffect 2');**
        };
    }, []);
    return <div>{ `width is ${width}` }</div>;
}

지금은 빈배열을 입력했기때문에 unmount되기 직전에만 이 반환한 함수(console.log('useEffect 2'))가 실행된다.

증가button을 클릭하면 <WidthPrinter/>가 unmount된다.

빈배열을 입력하지 않으면 width 상태값이 변경될 때마다 <WidthPrinter/>가 렌더링되고

  1. 그때마다 부수 효과 함수가 실행( console.log('useEffect 1'))
  2. return뒤에 있는 함수가 실행(console.log('useEffect 2'))

1과 2가 반복됨

2.8 훅 직접 만들기

  1. 훅을 직접 만들어 사용하면 쉽게 로직을 재사용할 수 있다.
  2. 커스텀 훅의 이름은 use로 시작하는게 좋다. (코드 가독성이 높아지며 여러 리액트 개발 도구의 도움도 쉽게 받을 수 있음)

(1) 위 코드 중 Profile 컴포넌트에서 개별 훅으로 분리해보기

import React from 'react';
import useUser from './useUser';

export default function Profile({ userId }) {
    **const user = useUser(userId);**
    return ( 
        <div>
            {!user && <p>사용자 정보를 가져오는 중...</p>}
            {user && (
                <>
                    <p>{`name is ${user.name}`}</p>
                    <p>{`age is ${user.age}`}</p>
                </>
            )}
        </div> 
    );
}
import { useState, useEffect } from 'react';

export default function useUser(userId) {
    const [user, setUser] = useState(null);
    useEffect(() => {
        getUserApi(userId).then(data => setUser(data));
    }, [userId]);
    return user;
}

const USER1 = { name: 'mike', age: 23 }; 
const USER2 = { name: 'jane', age: 31 }; 
function getUserApi(userId) { 
    return new Promise(res => { 
        setTimeout(() => {
            res(userId % 2 ? USER1 : USER2);
        }, 500);
    });
}

기존 훅(useState, useEffect)을 이용해서 자신만의 새로운 훅을 만들 수 있다는 점이 훅의 매력이다.

훅은 함수를 호출하는 것에서 끝나기 때문에 사용하기 편리

위 코드에서

  1. useId가 변경되면 훅 내부에서 자동으로 api를 호출해 사용자 데이터를 가져온다
  2. userId 내부 값이 변경되면 자동으로 Profile 컴포넌트도 같이 새로운 userId와 함께 렌더링 된다

장점

  1. 위 여러 가지 동작들이 Profile 컴포넌트 입장에서는 모른 채로 간편하게 사용할 수 있다는 것
  2. user 데이터를 가져오는 것은 비동기지만 마치 userId를 넣으면 user가 나오는 것처럼 동기 프로그램 방식처럼 간편하게 작성 할 수 있음

(2) 현업에서 사용할만한 훅

  1. useMounted()

    import { useState, useEffect } from 'react';
    
    export default function useMounted() {
        const [mounted, setMounted] = useState(false);
        useEffect(() => {
            setMounted(true);
        }, []);
        return mounted;
    }

    mount여부를 알려주는 훅

  2. useBlockIfNotLogin()

    로그인된 사용자만 접근할 수 있는 페이지에 사용하는 훅

  3. useBlockUnsavedChange(desc)

    사용자가 문서를 작성하다가 저장하지 않고 페이지를 벗어나려고할 때 경고를 띄어주는 훅

  4. useEffectIfLoginUser(callback, deps)

    useEffect를 실행하는데 로그인 유저인 경우에만 실행하고 싶을 때 사용하는 훅

  5. useLocalStorage(key, initialValue) ⇒ [value, setValue])

    key랑 초기값을 입력해주면 현재 value와 setValue함수를 반환해주는 훅

2.9 훅 사용 시 지켜야 할 규칙

(1) 하나의 컴포넌트에서 훅을 호출하는 순서는 항상 같아야한다.

function MyComponent() {
    const [value, setValue] = useState(0);

    if (value === 0) { //if문에서 사용 불가
        const [v1, setv1] = useState(0);
    } else {
        const [v1, setv1] = useState(0); 
        const [v2, setV2] = useState(0);
    }
    // ... 
    for (let i = 0; i < value; i++) {  //for문에서 사용 불가
        const [num, setNum] = useState(0);
    }
    // ...
    function func1() {  //함수에서 사용 불가
        const [num, setNum] = useState(0);
    }
    // ...
}
  1. if문 안에서 훅을 사용 불가

    훅은 항상 같은 순서로 호출되어야하기때문

    (어쩔 때는 한번 사용, 어쩔 때는 두번 사용하면 안되기 때문)

  2. for문 안에서 훅을 사용 불가

    value에 따라서 useState를 사용하는 횟수가 달리지기 때문

  3. 함수 안에서 훅 호출 불가

    함수가 항상 호출된다는 보장이 없기 때문

    (어쩔 때는 호출, 어쩔 때는 미호출하면 안되기 때문)

function Profile() {
    const [name, setName] = useState('mike');
    const [country, setCountry] = useState('korea');
    // ...
}

우리가 useState에 전달한 정보는 기본값밖에 없다 ('mike', 'korea')

useState뿐만 아니라 useEffect도 별다른 정보를 제공하지 않는다. 따라서 리액트 입장에서는 순서가 중요하다

첫 번째 사용된 useStat와 두 번째 사용된 useState를 구분하는 방법은 순서밖에 없기 때문

let hooks = null;

export function useHook() {
    // ... 
    hooks.push(hookData);
}

function process_a_component_rendering(component) { //컴포넌트를 받아서 렌더링하는 함수
    hooks = [];                                     //hooks라는 데이터 빈 배열로 초기화
    component();                                    //컴포넌트 실행
    let hooksForThisComponent = hooks;              //실행된 컴포넌 내부에서 만들어진 hooks를 저장
    hooks = null; 
    // ...
}

hooks라는 것이 만들어지는 것은 컴포넌트 내부에서 훅을 사용할 때 useHook함수 안에서 어떤 데이터를 만들어서 hooks라는 곳에 데이터를 넣는 구조

이렇게 넣을 때 자신의 순서에 맞게 차곡차곡 쌓임

(2) 훅은 함수형 컴포넌트 또는 커스텀 훅 안에서만 호출되어야 한다.

클래스형 컴포넌트의 메서드 뿐만 아니라 기타 일반 함수에서도 사용할 수 없음

profile
끄적끄적....

0개의 댓글