[React]focus된 input element에서 텍스트 뒤로 커서 보내기

HW·2023년 3월 7일
0

React

목록 보기
5/5
post-thumbnail

서론

BubblePop 프로젝트 중 사용자 키보드 입력을 화면에 말풍선 안에 보여줘야 합니다.
그런데 'ㄱㄴㄷㄻㅄ'를 입력하기 위해 한글자씩 입력할 때마다 커서가 텍스트 앞으로 가면서 'ㅅㅂㅁㄹㄷㄴㄱ'가 입력되게 됩니다.
입력의 역순으로 텍스트가 보이게 되는 셈이죠.

본론

소스코드

function App() {
  const [newText, setNewText] = useState('')

  useEffect (()=>{
    const  el =document.querySelector('.bubble.input > div');
    if (el){
      el.focus();
    }
  },[])
  return (
    <AppDiv>
      <Container>
        <NewMessage
          value={newText}
          onChange={setNewText}
        />
      </Container>
    </AppDiv>
  )
}

const AppDiv = styled.div`
  background-color: #00ff00;
  width: 1920px;
  height: 1080px;
  display: flex;
`

export default App;
import React, { useCallback } from 'react';
import styled from 'styled-components';
import Content from './NContent';

const NewMessage = ({value, onChange}) => {

    const handleChange = useCallback (
        e => {
            console.log("eee", e.target.value);
            onChange && onChange (e.target.value);
        },
        [onChange]
    );


    console.log ('test text : ', value);
    return (
        <InputMessage 
        className={`bubble input ${value.length === 0 ? 'empty' : ''}`}
        >
            <Content
                value={value}
                onChange={handleChange}

            />
        </InputMessage>
    )
}

export const InputMessage = styled.div`
  transition: opacity 0.4s ease-in-out;
  opacity: 1;
  border: none;
  outline: none;
  background-color: #000;
  font-size: 32px;
  min-width: 30px;
  color : #FFF;
  &.submitted {
    transition: none;
  }
  &.empty {
    opacity: 0;
  }
`;

export default NewMessage;
import React, { useRef } from 'react';

const Content = ({ value, onChange}) => {
  const refElement = useRef(null);
  let lastValue = value;
  const emitChange = () => {
    const div = refElement.current;
    const value = div.innerText;
    console.log ("HERE is change", value)
    if (onChange && value !== lastValue) {
      onChange({
        target: {
          value,
        },
      });
    }
    lastValue = value;
  };

  return (
    <div
      ref={refElement}
      onInput={emitChange}
      onBlur={emitChange}
      contentEditable
      spellCheck="false"
      dangerouslySetInnerHTML={{ __html: value }}
    ></div>
  );
};

export default Content;

의심이 가는 코드 부분입니다. 아무래도 컴포넌트에 focus를 하면서 커서가 앞으로 이동하는게 아닌가 싶은데요.

그래서
1. input에 있던 값을 변수에 저장
2. input에 focus 해주고
3. input의 value 값을 비워준 다음에
4. 다시 value 값을 채웁니다

그런데 여전히 커서는 야속하게 앞에 가 있네요.
다시 생각해보니 useEffect 두번째 매개변수로 []를 넣어주어

첫 렌더링에만 호출이 되어 그 이후에는 절대 재호출 되지 않기 때문에 의심할 필요가 없었습니다.


그렇다면 App Component는 재호출 되지 않기 때문에, 수정사항에서 제외하고, NewMessage Component부터 살펴보기로 합니다.

살펴보니 NewMessage Component는 useCallback 밖에 없는데요.
useCallback Hook은 특정 함수를 재선언하지 않고도 재사용 할 때 쓰입니다.
그래서 NewMessage Component는 사용자의 키보드 입력값(value), 그리고 사용자가 호출 되는 함수 (onChange)를 props로 App과 소통합니다.


결국 NewMessage Component는 Content Component props의 state가 바뀔 때 hadler들이 호출 되므로 Content Component를 살펴봐야 할 것 같습니다.


그래서 Content Component를 살펴보면,
사용자가 키보드 입력할 때, emitChange 화살표 함수를 호출하는데,
여기서 NewMessage의 handleChange가 호출되고,
사용자가 지금껏 입력한 문자열을 setNewText를 통해 newText로 저장합니다.
이 이유는 App Component에서 newText를 setState 함수 (useState)로 정의했기 떄문이죠.


setState 함수가 호출되면서 props 변경 여부와 관계없이 모든 하위 컴포넌트들이 re-rendering 되는데 이 과정에서 Content Component가 re-rendering 될 때,
그 동안 사용자가 입력했던 문자열은 props.value로 가져오되,
view가 rendering 되면서 커서가 문자열 앞으로 가게 된게 아닌가 싶습니다.


즉 문제 해결을 위해 re-rendering을 방지하면 됩니다.

해결

그래서 re-rendering을 방지하는 방법을 찾아보았습니다.

두가지 옵션이 있더군요.
1. React.memo Hook
2. shouldComponentUpdate

첫번째 옵션, React.memo Hook useMemo로서, 결과를 캐싱하고 다음 작업에서 캐싱했던 결과를 재사용하여 작업의 속도를 높이는 자바스크립트 기술 (Memoization)을 기반으로, CPU 소모가 심한 함수들을 캐싱하기 위해 쓰입니다.
만약 컴포넌트 내의 어떤 함수가 값을 리턴하는데, 이 함수의 리턴값은 re-rendering 될 때마다 호출될 것입니다.
또 그 함수가 return 되는 값이 자식 컴포넌트에도 사용이 된다면,
그 자식 컴포넌트도 함수가 호출 될 때마다 recursive하게 새로운 값을 받아 re-rendering 됩니다. 그래서 이를 방지하기 위해 위의 소스코드처럼 인라인 함수를 넘겨주는게 아닌, 콜백 함수 자체를 넘겨주는 것이죠.

다음과 같이 export default 부분에서 memo로 감싸주면 됩니다.

import React, { useRef } from 'react';

const Content = ({ value, onChange}) => {
  const refElement = useRef(null);
  let lastValue = value;
  const emitChange = () => {
    const div = refElement.current;
    const value = div.innerText;
    console.log ("HERE is change", value)
    if (onChange && value !== lastValue) {
      onChange({
        target: {
          value,
        },
      });
    }
    lastValue = value;
  };

  return (
    <div
      ref={refElement}
      onInput={emitChange}
      onBlur={emitChange}
      contentEditable
      spellCheck="false"
      dangerouslySetInnerHTML={{ __html: value }}
    ></div>
  );
};

export default React.memo(Content);

두번째 옵션, shouldComponentUpdate는 리턴값이 true냐 false냐에 따라 렌더링 여부를 결정하는 메소드입니다. state가 변경 되거나 부모 컴포넌트로부터 새로운 props를 전달받을 때 실행되는 메소드입니다. 메소드이기 때문에 클래스 기반 컴포넌트에서만 사용할 수 있습니다.

import React, {createRef} from 'react'

class Content extends React.Component {
  constructor(props) {
    super(props)
    this.refElement = createRef(null)
  }
  render() {
    return (
      <div
        ref={this.refElement}
        onInput={this.emitChange}
        onBlur={this.emitChange}
        contentEditable
        spellCheck="false"
        dangerouslySetInnerHTML={{ __html: this.props.value }}
      ></div>
    )
  }

  shouldComponentUpdate(nextProps) {
    const { current: div } = this.refElement
    console.log("다시?", (nextProps.value !== div.innerText))
    return nextProps.value !== div.innerText
  }

  emitChange = () => {
    const div = this.refElement.current
    var value = div.innerText
    if (this.props.onChange && value !== this.lastValue) {
      this.props.onChange({
        target: {
          value
        }
      })
    }
    this.lastValue = value
  }
}

export default Content

결론

커서를 텍스트 뒤로 보내는 간단한 문제로 보였지만, 이를 해결하기 위한 그 이면에는 아주 중요한 개념이 숨어있었습니다.
소스코드 상에서 컴포넌트들은 여러번 재사용할 수 있도록 universal & general 해야 합니다. 클래스 기반 컴포넌트와 달리, 함수 기반 컴포넌트는 수명 주기 메서드가 없어, 코드를 재사용할 수가 있는데요. 하지만 함수 기반 컴포넌트는 항상 React 렌더 주기 동안 re-rendering 되기 때문에, 이번 문제처럼 기능적 이슈를 일으키고, 렌더링이 많아지기 때문에 속도가 저하되고 메모리 사용량이 높아집니다.
이를 최적화 하기 위해서는 React.memo Hook은 필수불가결해 보입니다. 다만 사용할 때, memoization을 위한 전용 메모리가 추가로 필요하게 되고, 최적화를 위한 연산이 불필요한 경우엔 비용만 발생하기 때문에 무분별한 사용은 지양해야 합니다.

profile
예술융합형 개발자🎥

0개의 댓글