TIL | #9 React | Ref, 배열과 key

trevor1107·2021년 3월 31일
0

2021-03-31(수)

들어가기 전에

// App.js
import React, { Component } from 'react';
import ValidationSamplefrom './ValidationSample';
export class App extends Component {
    render() {
        return (
            <div>
                <ValidationSample/>
            </div>
        );
    }
}
export default App;

위와 같이 App 클래스에서 별 다른 조작 없이 컴포넌트를 포함만 시키는 경우는 App.js를 생략하겠다.

Ref

일반적인 React의 흐름에서 props는 부모 컴포넌트가 자식과 상호작용할 수 있는 유일한 수단이며, 자식을 수정하려면 새로운 props를 전달하여 자식을 다시 렌더링해야 한다. 그러나, 일반적인 흐름에서 벗어나 직접적으로 자식을 수정해야 하는 경우도 가끔씩 있는데, 수정할 자식은 React 컴포넌트의 인스턴스일 수도 있고, DOM 엘리먼트일 수도 있다. 그 해결책이 바로 Ref이다.

공식 입장에서 Ref의 바람직한 사용 사례는 다음과 같다.

  • 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때.
  • 애니메이션을 직접적으로 실행시킬 때.
  • 서드 파티 DOM 라이브러리를 React와 같이 사용할 때.

선언적으로 해결될 수 있는 문제에서는 ref 사용을 지양해야한다.

예를 들어, Dialog 컴포넌트에서 open()과 close() 메서드를 두는 대신, isOpen이라는 prop을 넘겨주세요.

ref는 애플리케이션에 “어떤 일이 일어나게” 할 때 사용될 수도 있다. 그럴 때는 잠시 멈추고 어느 컴포넌트 계층에서 상태를 소유해야 하는지 신중하게 생각해보자. 대부분의 경우, 더 높은 계층에서 상태를 소유해야한다.

특정 인풋에 포커스, 스크롤 박스 조작, 캔버스에 그림 그리기 등
state만으로 해결할 수 없는 경우에는 DOM에 직접 접근해야 한다.

ref 사용하는 방법

1. 콜백함수를 통한 ref설정

2. createRef() : 리액트 내장함수, 16.3버전에 추가되었다.

리액트에서 컴포넌트에도 ref가 가능하다. 주로 컴포넌트 내부에 있는 DOM을 컴포넌트 외부에서 접근하고자 할때 사용한다.

<MyComponent ref={(ref)=>{this.MyComponent=ref}}

MyComponent 내부의 메서드, 멤버 변수에도 접근할 수 있다. 내부의 ref에도 접근할 수 있다.

MyComponent.input, MyComponent.handleClick

1. 콜백 ref

콜백 ref의 주의 사항

ref 콜백이 인라인 함수로 선언되있다면 ref 콜백은 업데이트 과정 중에 처음에는 null로, 그 다음에는 DOM 엘리먼트로, 총 두 번 호출된다. 이러한 현상은 매 렌더링마다 ref 콜백의 새 인스턴스가 생성되므로 React가 이전에 사용된 ref를 제거하고 새 ref를 설정해야 하기 때문에 일어난다. 이러한 현상은 ref 콜백을 클래스에 바인딩된 메서드로 선언함으로써 해결할 수 있다.

하지만 많은 경우 이러한 현상은 문제가 되지 않는다고 한다.

// ScrollBox.js

import React, { Component } from 'react';

export class ScrollBox extends Component {
    scrollToBottom = () => {
        // const scrollHeight = this.box.scrollHeight;
        // const clientHeight = this.box.clientHeight;

        // 위와 같은 의미의 ES6 비구조화 문법
        const { scrollHeight, clientHeight } = this.box;

        this.box.scrollTop = scrollHeight - clientHeight;
    };

    render() {
        const style = {
            position: 'relative',
            overflow: 'auto',
            width: '300px',
            height: '300px',
            border: '1px solid black',
        };
        const innerStyle = {
            width: '100%',
            height: '650px',
            background: 'linear-gradient(white, black)',
        };

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

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

export class App extends Component {
    render() {
        return (
            <div>
								{// 여기서 this는 class App이고 컴포넌트를 ref에 포함시킨다.}
                <ScrollBox ref={(ref) => {this.ScrollBox = ref;}}
                />
								{// ScrollBox 내부의 scrollTomBottom 함수를 호출한다.}
                <button onClick={() => this.ScrollBox.scrollToBottom()}>
                    밑으로
                </button>
            </div>
        );
    }
}

export default App;

2. createRef()

// ValidationSample.js

import React, { Component } from 'react';
import './ValidationSample.css';
export class ValidationSample extends Component {
    constructor(props) {
        super(props);
        this.input = React.createRef();
    }
    state = {
        password: '',
        clicked: false,
        validated: false,
    };
    handleChange = (e) => {
        this.setState({ password: e.target.value, });
    };
    handleButtonClick = () => {
        this.setState({
            clicked: true,
            validated: this.state.password === '0000',
        });
				// createRef로 생성된 경우 current로 DOM Element에 접근할 수 있다.
        this.input.current.focus();
    };
    render() {
        return (
            <div>
                <input
                    ref={this.input}
                    type="password"
                    value={this.state.password}
                    onChange={this.handleChange}
                    className={
                        this.state.clicked
                            ? this.state.validated
                                ? 'success'
                                : 'failure'
                            : ''
                    }
                />
                <button onClick={this.handleButtonClick}>입력</button>
            </div>
        );
    }
}

export default ValidationSample;

콜백 ref와 createRef()의 차이점

createRef()의 경우 ref 속성이 쓰인 DOM Element 또는 Component의 데이터가 새로 생성된 인스턴스 current 프로퍼티로 값을 받는다. ref 속성을 지정하는 방법은 ref = {this.input} 처럼 하는 것이다. 그리고 current 프로퍼티를 통해 접근하여 사용한다.

반면 콜백 ref는 함수의 특성으로 인스턴스를 가질 수 없기 때문에, 함수 컴포넌트에 ref 속성을 사용할 수 없다. 그래서 DOM Element로 함수가 한번 더 호출되고 새로운 인스턴스를 생성하는 것이다. 그리고 current를 거치지 않고 바로 접근해서 사용한다.

함수 컴포넌트에서 사용하는 방법

function MyFunctionComponent() {
  return <input />;
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  render() {
    // 이 코드는 동작하지 않는다.
    return (
      <MyFunctionComponent ref={this.textInput} />
    );
  }
}

잘 될 것 같아 보이지만 올바르지 못한 사용 방법이다.

함수 컴포넌트에 ref를 사용할 수 있도록 하려면, forwardRef (높은 확률로 useImperativeHandle와 함께)를 사용하거나 클래스 컴포넌트로 변경할 수 있다.

다만, DOM 엘리먼트나 클래스 컴포넌트의 인스턴스에 접근하기 위해 ref 어트리뷰트를 함수 컴포넌트에서 사용하는 것은 가능하다.

function CustomTextInput(props) {
  // textInput은 ref 속성을 통해 전달되기 위해서
  // 이곳에서 정의되어야 한다.
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }

  return (
    <div>
      <input
        type="text"
        ref={textInput} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );
}

배열과 Key

React에서 key는 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 어트리뷰트이다.

Key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는다. key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 한다.

배열 데이터를 효율적으로 출력하는 방법

// IterationSample.js

import React from 'react';

// 배열의 데이터를 출력하는 방법
const IterationSample = () => {
    const names = ['얼음', '눈사람', '미세먼지'];

    // 아래의 map형태는 key prop이 없다는 경고를 내뱉는다.
    // const nameList = names.map((name) => <li>{name}</li>);

    // 키값을 넣어주어야 한다.
    const nameList = names.map((name, index) => <li key={index}>{name}</li>);

    return <ul>{nameList}</ul>;
};

export default IterationSample;

만약 key가 없으면 virtual DOM을 비교하는 과정에서 리스트를 순차적으로 비교한다.
key가 있다면, 이 값을 이용해서 어떤 변화가 일어났는지 빠르게 알아낼 수 있다.

key 값을 설정할 때는 map함수의 인자로 전달되는 함수 내부에서 컴포넌트 props를 설정하듯이 하면 된다.

const articleList = articles.map((article) => (
    <Article title={article.title} writer={article.writer} key={article.id} />
));

Key를 선택하는 가장 좋은 방법은 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것이다. 대부분의 경우 데이터의 ID를 key로 사용할 것을 권장한다.

배열 객체의 처리

배열의 요소가 객체인 데이터 state를 가지고, 목록을 표시하고 입력 을 통해서 state에 데이터를 추가한다.

추가로 목록의 텍스트를 더블 클릭하면 삭제시켜주는 기능까지 포함한다.

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

const IterationSample = () => {
    const [names, setNames] = useState([
        { id: 1, text: '김치찌개' },
        { id: 2, text: '부대찌개' },
        { id: 3, text: '탕수육' },
    ]);
    // 인풋 상태
    const [inputText, setInputText] = useState('');
    // 새로운 항목을 추가할 때 사용할 Id
    const [nextId, setNextId] = useState(names.length + 1);

    const onChange = (e) => setInputText(e.target.value);
    const onClick = (e) => {
        const nextNames = names.concat({
            id: nextId,
            text: inputText,
        });
        setNextId(nextId + 1);
        setNames(nextNames);
        setInputText('');
    };
    const onKeyPressEnter = (e) => {
        if (e.key === 'Enter') {
            onClick();
        }
    };
    const onDelete = (index) => {
        const nextNames = names.filter((value) => value.id !== index);
        setNames(nextNames);
    };

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

    return (
        <div>
            <input
                value={inputText}
                onChange={onChange}
                onKeyPress={onKeyPressEnter}
            />
            <button onClick={onClick}>추가</button>
            <ul>{nameList}</ul>
        </div>
    );
};

export default IterationSample;

불변성 유지

불변성 유지로 리액트 컴포넌트의 성능을 최적화할 수 있다.  쉽게 설명하자면 기존의 값의 복사본을 받아 새로운 값으로 덮어씌우는 방식으로 설계하는 것이다.

우선 리액트에서는 이전 상태값과 바꿀 상태값을 비교해야 하기 때문에, setState함수를 통해서 상태를 변경시켜야 적용된다. state를 바꾸는 과정에서 원본을 건드리게되면, 참조하고 있는 다른 변수들에 의해 불변성 유지에 어긋나 제대로 된 비교가 이루어지지 않는다.

불변성을 유지하는 것에 대한 단점은 복사본이 필요해 코드가 길어지고, 복잡한 구조가 될 수 있다는 것이다.

이에 대한 대안으로 불변성을 유지시켜주는 라이브러리 immutable, immer가 있다.



참고자료 및 사이트 (감사합니다)

profile
프론트엔드 개발자

0개의 댓글