WEB🔨/React

리액트를 다루는 기술 요약#3

최문경 블로그 2021. 6. 21. 15:27

해당 시리즈는 제목 그대로 김민준님의 '리액트를 다루는 기술'을 요약한 글입니다.

 

목차

5장 ref: DOM에 이름달기

6장 컴포넌트 반복

 

5장 ref: DOM에 이름 달기

특정 DOM 요소에 어떤 작업을 해야 할 때 요소에 id를 달면 CSS에서 특정 id에 스타일을 적용하거나 자바스크립트에서도 작업할 수 있습니다. 이렇게 요소에 id를 사용해 이름을 다는 것처럼 리액트에서는 ref(reference)를 사용해 DOM에 이름을 달 수 있습니다.

 

하지만 대체 '어떤' 작업을 할 때 ref를 사용해야 할까요?

 

정답은 'DOM을 꼭 직접적으로 건드려야 할 때' 입니다.

 

예를 들어, 다음과 같이 state만으로는 해결할 수 없는 기능에서는 어쩔 수 없이 ref를 사용해 DOM에 직접적으로 접근해야 합니다.

 

  • 특정 input에 포커스 주기
  • 스크롤 박스 조작하기
  • Canvas 요소에 그림 그리기 등

 

콜백 함수를 통한 ref 설정

ref를 만드는 가장 기본적인 방법은 콜백 함수를 사용하는 것. (ref를 달고자 하는 요소에 ref라는 콜백함수를 props로 전달해주면 이 콜백함수는 ref값을 파라미터로 전달받고 함수 내부에서 멤버 변수로 설정)

<input ref={ref => {this.input=ref}} />

앞으로 this.input은 input 요소의 DOM을 가르킴.

 

 

createRef를 통한 ref 설정

콜백 함수를 사용하는 것보다 더 적은 코드로 사용 가능.

import React, { Component } from 'react';

class RefSample extends Component {
    input = React.createRef();
    
    handleFocus = () => {
    	this.input.current.focus();
    }
    
    render() {
    	return (
            <div>
            	<input ref={this.input} />
            </div>
        )
    }
}

export default RefSample;

 

 

컴포넌트에 ref 달기

리액트에서는 컴포넌트에도 ref를 달 수 있는데, 주로 컴포넌트 내부에 있는 DOM을 컴포넌트 외부에서 사용할 때 씁니다.

<MyComponent ref={ref => this.myComponent=ref}} />

이렇게 하면 MyComponent 내부의 메서드 및 멤버 변수에도 접근할 수 있습니다. 즉, 내부의 ref에도 접근할 수 있습니다.

 

import React, { Component } from 'react';

class ScrollBox extends Component {
  scrollToBottom = () => {
    const { scrollHeight, clientHeight } = this.box;
    this.box.scrollTop = scrollHeight - clientHeight;
  };

  render() {
    const style = {};
    const innerStyle = {};

    return (
      <div
        style={style}
        ref={(ref) => {
          this.box = ref;
        }}
      >
        <div style={innerStyle} />
      </div>
    );
  }
}

export default ScrollBox;
import React, { Component } from 'react';
import ScrollBox from './ScrollBox';

class App extends Component {
  render() {
    return (
      <div>
        <ScrollBox ref={(ref) => (this.scrollBox = ref)} />
        <button onClick={() => this.scrollBox.scrollToBottom()}>
          맨 밑으로
        </button>
      </div>
    );
  }
}

export default App;

 

여기서 주의할 점이 있습니다. 문법상으로는 onClick = {this.scrollBox.scrollBottom}과 같은 형식으로 작성해도 틀린 것은 아니지만 컴포넌트가 처음 렌더링될 때는 this.scrollBox 값이 undefined이므로 this.scrollBox.scrollToBottom 값을 읽어 오는 과정에서 오류가 발생합니다.

 

정리

컴포넌트 내부에서 DOM에 직접 접근해야 할 때는 ref를 사용합니다. 먼저 ref를 사용하지 않고도 원하는 기능을 구현할 수 있는지 반드시 고려한 후에 활용하세요. 함수형 컴포넌트에서 ref를 사용하는 것은 8장에서 배우겠습니다.

 

 

6장 컴포넌트 반복

웹 애플리케이션을 만들다 보면 다음과 같이 반복되는 코드를 작성할 때가 있습니다.

import React from 'react';

const IterationSample = () => {
  return (
    <ul>
      <li>눈사람</li>
      <li>얼음</li>
      <li>눈</li>
      <li>바람</li>
    </ul>
  );
};

export default IterationSample;

 

 

다음과 같이 자바스크립트 배열 객체의 내장 함수인 map 함수를 사용하여 반복되는 컴포넌트를 렌더링할 수 있습니다.

import React from 'react';

const IterationSample = () => {
  const names = ['눈사람', '얼음', '눈', '바람'];
  const nameList = names.map((name) => <li>{name}</li>);
  return <ul>{nameList}</ul>;
};

export default IterationSample;

하지만 위와 같이 각각의 child는 unique한 "key" prop을 가져야 한다고 합니다.

 

 

Key

key가 없을 때는 Virual DOM을 비교하는 과정에서 리스트를 순차적으로 비교하면서 변화를 감지합니다. 하지만 key가 있다면 이 값을 사용하여 어떤 변화가 일어났는지 더욱 빠르게 알아낼 수 있습니다.

import React from 'react';

const IterationSample = () => {
  const names = ['눈사람', '얼음', '눈', '바람'];
  const nameList = names.map((name, index) => <li key={index}>{name}</li>);
  return <ul>{nameList}</ul>;
};

export default IterationSample;

이제 개발자 도구에서 더 이상 경고 메시지를 표시하지 않습니다. 하지만 고유한 값이 없을 때만 index값을 key로 사용해야 합니다. 왜냐하면 index를 key로 사용하면 배열이 변경될 때 효율적으로 리렌더링하지 못하기 때문입니다.

 

 

응용

import React, { useState } from 'react';

const IterationSample = () => {
  const [names, setNames] = useState([
    { id: 1, text: '눈사람' },
    { id: 2, text: '얼음' },
    { id: 3, text: '눈' },
    { id: 4, text: '바람' },
  ]);
  const [inputText, setInputText] = useState('');
  const [nextId, setNextId] = useState(5);

  const onChange = (e) => setInputText(e.target.value);
  const onSubmit = (e) => {
    e.preventDefault();
    const nextNames = names.concat({
      id: nextId,
      text: inputText,
    });
    setNames(nextNames);
    setNextId(nextId + 1);
    setInputText('');
  };

  const namesList = names.map((name) => <li key={name.id}>{name.text}</li>);
  return (
    <>
      <form onSubmit={onSubmit}>
        <input type="text" value={inputText} onChange={onChange} />
        <button type="submit">추가</button>
      </form>
      <ul>{namesList}</ul>
    </>
  );
};

export default IterationSample;

배열에 새 항목을 추가할 때 push함수를 사용하지 않고 concat을 사용했는 데요. push 함수는 기존 배열 자체를 변경해주는 반면, concat은 새로운 배열을 만들어 준다는 차이점이 있습니다. 리액트에서 상태를 업데이트할 때는 기존 상태를 그대로 두면서 새로운 값을 상태로 설정해야 합니다. 이를 불변성 유지라고 하는데요. 불변성 유지를 해 주어야 나중에 리액트 컴포넌트의 성능을 최적화할 수 있습니다.

 

 

데이터 제거 기능 구현하기

import React, { useState } from 'react';

const IterationSample = () => {
  const [names, setNames] = useState([
    { id: 1, text: '눈사람' },
    { id: 2, text: '얼음' },
    { id: 3, text: '눈' },
    { id: 4, text: '바람' },
  ]);
  const [inputText, setInputText] = useState('');
  const [nextId, setNextId] = useState(5);

  const onChange = (e) => setInputText(e.target.value);
  const onSubmit = (e) => {
    e.preventDefault();
    const nextNames = names.concat({
      id: nextId,
      text: inputText,
    });
    setNames(nextNames);
    setNextId(nextId + 1);
    setInputText('');
  };
  const onRemove = (id) => {
    const nextNames = names.filter((name) => name.id !== id);
    setNames(nextNames);
  };

  const namesList = names.map((name) => (
    <li key={name.id} onDoubleClick={() => onRemove(name.id)}>
      {name.text}
    </li>
  ));

  return (
    <>
      <form onSubmit={onSubmit}>
        <input type="text" value={inputText} onChange={onChange} />
        <button type="submit">추가</button>
      </form>
      <ul>{namesList}</ul>
    </>
  );
};

export default IterationSample;

 

 

정리

상태 안에서 배열을 변형할 때는 배열에 직접 접근하여 수정하는 것이 아니라 concat, filter 등의 배열 내장 함수를 사용하여 새로운 배열을 만든 후 이를 새로운 상태로 설정해 주어야 한다는 것을 명심하세요.