View on GitHub

FireFours

React Study: 발등에 불떨어진 4학년들의 모임

1. 배운 내용

리액트 시작

JSX

컴포넌트

이벤트 핸들링

ref: DOM에 이름 달기

컴포넌트 반복

컴포넌트의 라이프사이클 메서드

Hooks



2. 질문 공유와 해결

useRef()

-일반적인 사용방법

useRef() Hook은 함수형 컴포넌트에서 ref를 쉽게 사용할 수 있도록 해주는 용도로 사용한다. 이 메서드를 사용하는 방법은 다음과 같다.

 const ref = useRef(initialValue)

useRef는 .current 프로퍼티로 전달된 인자로 초기화된 변경 가능한 ref객체를 반환한다.

일반적인 사용용도는

 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>
    </>
  );
}
 

와 같이 DOM의 특정 Element를 참조를 저장하기 위해 사용한다.

위 코드에서

 <input ref={inputEl} type="text" />

를 실행하게 되면 input.current에는 input객체의 참조가 할당된다. 따라서

 input.current.something()

과 같이 해당 DOM element에 접근 해줄 수 있는 것이다.

-다른 사용 방법

useRef()는 컴포넌트 내에서 로컬변수를 사용해야 할 때도 사용된다.

사용예를 보자면

import React,{useRef} from 'react';

const RefSample=()=>{
    const id=useRef(1);
    const setId=(n)=>{
        id.current=n;
    }
    const printId=()=>{
        console.log(id.current);
    }
    return(
        <div>
        refSample
        </div>
    );
};

export default RefSample

이렇게 코드를 작성하면

const id=useRef(1);

이렇게 초기화된 useRef의 파라메터1은 id.current에 할당된다.

이렇게 할당된 값은 2가지 특징을 가진다.

  1. 해당컴포넌트가 업데이트 되어도 값을 유지(full LifeTime)
  2. 해당값을 변경해도 컴포넌트가 다시 렌더링되지 않는다.

이러한 특징 때문에 컴포넌트 내부에서 컴포넌트가 재 랜더링이 되어도 컴포넌트의 모든 lifetime 동안 지속적으로 값을 유지하고 있는 변수로서 사용하기도 한다.

Q:

여기서 한가지 의문이 생길 수 있다. 컴포넌트 내부에서

import React,{useRef} from 'react';

const RefSample=()=>{
    let id=1;//이부분을 유심히 보자.
    const setId=(n)=>{
        id.current=n;
    }
    const printId=()=>{
        console.log(id.current);
    }
    return(
        <div>
        refSample
        </div>
    );
};

export default RefSample

과 같이 일반 let 변수를 이용해서 로컬변수를 이용하면 안되는 것인가?

A:

그냥, ‘로컬변수’를 이용할 것이라면 상관이 없다. 하지만 let을 이용해 로컬변수를 설정해줄 경우 컴포넌트가 다시 렌더링 될 때 마다 let id=1; 이 실행되기 때문에 id값은 항상 1이 될 것이다. 하지만 위에서 설명했듯이 useRef()를 이용해 값을 초기화 해줄경우 .current에 할당된 값은 해당 컴포넌트가 unmount될때 까지 유지때문에 해당 변수에 동적으로 업데이트 해준 값을 유지해줄 수 있다.


React에서 ref의 역할

ref는 특정 DOM에 작업을 사용할 때 사용한다. 비슷한 예로 HTML에서는 DOM 요소에 id를 달고 특정 id에 접근하여 스타일을 적용하거나 자바스크립트에서 해당 id를 가진 요소를 찾아서 작업을 한다. 다시 말해 ‘DOM을 직접적으로 건드려야 할 때’ ref를 사용한다. 그렇다면 특정 DOM에 작업을 하는 경우에는 어떤 것들이 있을까? 크게 세 가지가 있다.

  1. 특정 input/textarea에 포커스 주기
  2. 스크롤 박스 조작하기
  3. Canvas 요소에 그림 그리기 이 중에서 ‘Canvas 요소에 그림 그리기’라는 부분이 이해가 잘 되지 않았다. 처음엔 css 관련 작업을 할 때 해당 요소에 className을 지정하여 state를 조작하는 방식으로 이해했었다. 하지만, HTML에는 canvas라는 요소가 있다는 사실을 알 게 되었고 이 요소를 통해 그림을 그릴 때 ref가 사용된다는 것을 알 게 되었다. Jquery를 사용할 땐 해당 요소의 id를 통해 접근을 할 수 있었는데 리액트에선 React.createRef()라는 메서드를 통해 ref를 이용해야 한다. 왜냐하면 리액트는 virtual DOM을 사용하기 때문에 canvas와 같은 실제 엘리먼트에 접근하기 위해서는 ref를 통해 참조를 해야 한다. 아래 코드 참조.
    class refPrac extends Component {
    context = null;
    input = React.createRef();
      componentDidMount() {
     this.context = this.input.current.getContext(2d);
      }
      render() {
     return ( 
        <canvas ref={this.input} />
     )
      };
    }
    

12월 30일 내용

(7장) Q1. getSnapshotBeforeUpdate이 가지는 의미와 사용법

예시로 스크롤 위치를 위해 쓰인다는 말이 있지만 구체적으로 어떤 방식으로 사용되는지 이해가 안됐으나 React 공식 문서에서 그 사용법을 알 수 있었다.

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 어떤 값이 추가됐다면 그때의 스크롤 위치를 snapshot으로 반환
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      // snapshot 값으로 Return
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // snapshot을 통해 받아온 스크롤 위치로 바꿔준다. 
    // (위 getSnapshot.. 함수에서 넘어온 스크롤 값이 snapshot에 저장되어 있다)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

getSnapshotBeforeUpdate 와 componentDidUpdate는 둘 다 render가 이뤄진 후라는 점은 같으나 React DOM 및 refs 업데이트 의 전후라는 차이가 있다. state의 변화가 반영되어 있지만 실제 화면에 뿌려지기 전후라고 이해할 수 있다. 위 예시에선 DOM이 업데이트 되면서 스크롤이 최상단으로 가기 때문에 그 전에 그 위치를 저장하고 업데이트 이후 다시 그 위치로 변경시켜주는 것이다. 그 외에도 DOM 변화와 관련된 업데이트에 대해서 사용할 수 있을 것이다.

(4장) 이벤트핸들링 함수의 다른 방안

기존의 target.name을 활용한 onChange 함수에서 더 많은 인자를 받을 수 있는 함수로 변환해볼 수 있다.

  const [data, setData] = useState({user:'cho', pw: 123});
  
  // key를 받아 해당 key값의 state를 변경
  const onChange = key => e => {
    setData({...data, [key]: e.target.value});
  };
  
  const onChange2 = e => {
    setData({...data, [e.target.name]: e.target.value});
  };
  
  return (<>
    <input
    type="text"
    placeholder="유저명"
    value={data.user}
    onChange={onChange("user")} // {(event) => (onChnage("user"))(event)} 도 똑같이 동작한다.
  />
   <input
    type="text"
    name="pw"
    placeholder="패스워드"
    value={data.pw}
    onChange={onChange2}
  />)

(8장) useCallback 함수에 대한 심층 이해

useCallback 함수는 useMemo 함수와 비슷하게 어떤 state의 변화가 생겼을 때에만 함수가 생성되도록 하는 Hook이다. 이해가 힘들었던 것은 어떤 것은 처음 렌더링 될 때만 함수를 생성하고, 어떤 함수는 state 변화에 따라 함수를 생성하는데 이 차이가 무엇인지 와닿지 않았다. 이 그 두 함수는 아래와 같다.

  const onChange = useCallback(e => {
    setNumber(e.target.value);
    console.log('number:', number);
  }, []); // 컴포넌트가 처음 렌더링 될 때만 함수 생성

  const onInsert = useCallback(() => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
    inputEl.current.focus();
  }, [number, list]); // number 혹은 list 가 바뀌었을 때만 함수 생성

  return (
    <div>
      <input value={number} onChange={onChange} ref={inputEl} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {avg}
      </div>
    </div>
  );

이를 더 잘 이해하기 위해 몇가지 실험을 하였다. 먼저 console.log('number:', number); 를 통해 onChnage 속에서 number state를 보면 어떤 값을 입력하더라도 값이 없다고 나온다. 즉 onChange가 처음 생겼을 때 number 상태 그대로를 가지고 있다 반면 onInsert는 실행할 때마다 list, number의 값이 바뀌었다. 그리고 onChnage 두번째 인자로 [number]를 넣으면 number가 계속 변한다.

두번째로 onInsert의 두번째 인자를 모두 삭제하면 아래와 같이 된다.

  const onInsert = useCallback(() => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
    inputEl.current.focus();
  }, []); // 컴포넌트가 처음 렌더링 될 때만 함수 생성

위 상태에선 “Warning: Received NaN for the children attribute. If this is expected, cast the value to a string.” 란 오류가 뜬다. 이 말인 즉슨 Nan 을 li 에서 받아 경고창을 보냈다. 즉 위 함수에서 parseInt(number)의 number가 Nan이였기 때문이다. number의 처음상태가 빈값이라 그 이후에도 실행시켜도 빈값이 반환된 것이다.

정리하자면, 어떤 state의 값을 이용하여 새로운 값으로 변환하는 것이라면 state 변화에 따라 함수를 생성하고, static 하게 단순한 함수 실행이라면 처음 렌더링될때만 함수가 생성하도록 한다.