본문 바로가기
개발/리액트

[리액트] 특정 엘리먼트에 focus 주는 방법에 대하여(feat.타입스크립트)

by 핸디(Handy) 2021. 5. 10.

들어가며

특정 버튼을 눌렀을때 입력창이 뜨고 텍스트를 입력합니다.

근데 텍스트 입력이 안됩니다. 귀찮게 마우스로 입력창을 누르고 다시 키보드로 옮기는 과정이 필요합니다. 매우 귀찮죠.

그럴때 필요한 기능이 바로 리액트 컴포턴트에 focus를 주는 것입니다.

이번 글에서는 바로 이 기능에 대한 간단한 구현과 예시를 살펴보겠습니다.

자바스크립트에서의 Focus

일단 리액트의 focus를 들어가기 전에 HTML의 focus에 대해 간단히 살펴보겠습니다.

기본 사용법

 

HTMLElement.focus() - Web APIs | MDN

The HTMLElement.focus() method sets focus on the specified element, if it can be focused. The focused element is the element that will receive keyboard and similar events by default.

developer.mozilla.org

<input id="myTextField" value="Text field.">
<button id="focusButton">Click to set focus on the text field</button>
document.getElementById("focusButton").addEventListener("click", () => {
  document.getElementById("myTextField").focus();
});

예시 코드의 설명은   focusButton  click 하면  myTextField  focus 를 주는 코드입니다. 아주아주 간단하죠.

그리고 focus로 된 엘리먼트에 스크롤이 이동됩니다. 이게 Default 옵션이에요.

스크롤 막는 사용법

document.getElementById("myButton").focus({preventScroll:true}); // 이건 스크롤 막음

위 코드를 사용하면 스크롤은 막을 수 있는데 스크롤을 막은 focus는 별 의미가 없는것 같습니다. 그래서 한번도 사용해 본적이 없어요.

그럼 이제 React에서의 focus로 넘어가봅시다.

리액트에서의 Focus

자바스크립트에서 정확히 하자면 바닐라자바스크립트 상에서의 focus는 간편했습니다. 컴포넌트를 id로 찾아서 바로 focus를 해주면 됬거든요.

근데 리액트에서는 사알짝 골치가 아픕니다.

일단 리액트에서는 document에 직접적으로 접근을 권장하지 않습니다. v-dom를 쓰는데 dom에 달라붙으면 무슨 의미가,...

그래서 ref라는 prop과 useRef 라는 훅을 사용합니다. ( 클래스형 일때는 createRef인가 뭐시기가 있긴한데 뇌용량이 부족하니 넘어가요 )

일단 공식문서에서 한번 봐봅시다.

공식문서

https://ko.reactjs.org/docs/refs-and-the-dom.html#when-to-use-refs

 

 

Ref와 DOM – React

A JavaScript library for building user interfaces

ko.reactjs.org

당당히 첫번째에 포커스가 있네요. 아주 바람직합니다.

잠깐 3번째 서드 파티 DOM 라이브러리는 어떤 말이냐

제가 요새 작업하는 라이브러리 waveform-playlist에서와 같이 div에 ref를 걸어주고 옵션을 넣으면 해당 div에 라이브러리가 알아서 무언갈 만들어버립니다.

이럴때 적용해주는 예시라고 봐주시면 되겠습니다.

다시 돌아가 백문이불여일견이라 한번 컴포넌트 모습을 보고 확인하고 가시겠습니다.

동작 예시 (적용전)

아래는 동작화면입니다.

주식 추가 버튼을 눌렀을때 자동으로 input 엘리먼트로 focus가 되지않아 유저가 직접 눌러야만 하는 불편함 보입니다. 

또한 문제점이 하나 더 있는데요.

input를 클릭하여 텍스트를 입력하고 검색을 한다음 체크박스를 클릭하면 다시 focus가 풀려버립니다.

컴포넌트가 check됨에 따라 리랜더링되었고 그에 따라 포커스가 풀렸죠.

근데 실은 이 문제는 컴포넌트가 적절히 분리되어 있다면 발생하지 않을 것입니다. ㅎㅎ

근데 항상 최적화는 귀찮으니깐 컴퓨터 성능을 믿고 사용성만 개선해봐요.

동작 개선 

따라서 저는 개선방향을 잡았습니다.

  1. 주식 추가 버튼을 클릭했을때 input element가 focus 될 것.
  2. 테이블에서 주식을 클릭하여 추가했을때도 input element의 focus가 유지될 것.

다행스럽게도 현재 컴포넌트는 주식을 검색하던, 클릭하던 매번 랜더링되는 컴포넌트였기에 맨 처음 랜더링되었을때만 input 엘리먼트가 focus를 유지하고 있으면 되는 개선이 완료될 듯합니다.

import React, { useCallback, useRef, useLayoutEffect } from "react";

const SearchBarInput = ({ searchQuery, setSearchQuery }: searchBarProp) => {
  const classes = useStyles();
  const inputRef = useRef<HTMLInputElement>(null);
  useLayoutEffect(() => {
    if (inputRef.current !== null) inputRef.current.focus();
  });

  const onChange = useCallback(
    (e: any) => {
      setSearchQuery(e.target.value);
    },
    [setSearchQuery]
  );

  const inputClear = useCallback(() => {
    setSearchQuery("");
  }, [setSearchQuery]);
  return (
    <>
      <div className={classes.root}>
        <SearchIcon fontSize={"small"} style={{ marginTop: "2px", zIndex: 11 }} />
        <form action="/" method="get">
          <input
          	ref={inputRef}
            autoComplete="off"
            className={classes.input}
            value={searchQuery}
            onInput={onChange}
            type="text"
            id="header-search"
            placeholder="삼성전자 or 005930"
            name=""
          />
        </form>
        <HighlightOffIcon className={classes.closeIcon} fontSize={"small"} onClick={inputClear} />
      </div>
    </>
  );
};

중요한 부분이 바로 이 부분입니다. 전체 코드여서 눈에 잘 안들어오죠???

  const inputRef = useRef<HTMLInputElement>(null);
  useLayoutEffect(() => {
    if (inputRef.current !== null) inputRef.current.focus();
  });

useLayoutEffect를 사용했는데요. 실제로는 useEffect를 사용해도 별 상관은 없습니다.

다만 useEffect와 useLayoutEffect의 차이점에 대해 알고나서 사용하면 useLayoutEffect가 더욱 적절하다라는 생각이 듭니다.

화면이 paint되기 전에 실행되는 useLayoutEffect야 말로 컴포넌트가 유저에게 보이자마자 포커스되는 적절한 UX같기 때문입니다.

아래 문서는 인터넷에서 검색한 글이고 저와 같은 의견의 글입니다.

 

React useRef and useLayoutEffect vs useEffect (Step-By-Step Case Study)

Will go over how to use React useRef with useLayoutEffect vs using useEffect. React useRef is used to capture a DOM node reference to manipulate the the element directly, and this is done in this useLayoutEffect hook.

linguinecode.com

다음 문서는 useEffect와 useLayoutEffect에 대한 비교글입니다.

 

[리액트] useEffect와 useLayoutEffect | 비교시리즈

들어가며 리액트를 사용하다 보면 useEffect와 useLayoutEffect 훅을 마주하곤 합니다. 생긴 것도 비슷하고 실제 공식문서상에서도 둘은 같다고 말합니다. 오히려 useEffect 쓰라고 합니다. 그래도 useLayout

all-dev-kang.tistory.com

useEffect와 useLayoutEffect의 랜더링큐에 들어가는 순서까지 코드분석을 해봤으니 확인해보시면 재밌습니다. 

다시 focus로 돌아와

이제 원하는 대상에 ref를 넣어줍니다.

          <input
            ref={inputRef}
            autoComplete="off"
            className={classes.input}
            value={searchQuery}
            onInput={onChange}
            type="text"
            id="header-search"
            placeholder="삼성전자 or 005930"
            name=""
          />

이후에 focus를 주고싶은 element에 ref로 넣으면 됩니다.

이제 바뀐 컴포넌트를 보겠습니다.

동작 예시 (적용후)

계속 focus가 input 엘리먼트에 유지되는 것을 확인할 수 있습니다.

동작을 순서대로 살펴보기 위해 콘솔에 찍어보겠습니다.

보면 랜더링이 되고 inputRef가 null로 초기화된후 useLayoutEffect 이후에 current가 잡힌 것을 확인할 수 있습니다.

그럼 이제 한단계 더 나아가볼까요?

useFocus() 훅 만들기 

포커스를 하기 위해선 useLayoutEffect와 useRef라는 훅을 2개 써야합니다.  너무 귀찮죠.

그럴때 우리는 커스텀훅을 만들면 됩니다.

useFocus 코드

import { useLayoutEffect, useRef, useState } from "react";

const useFocus = (defaultFocused = false) => {
  const ref = useRef<HTMLElement>();
  const [isFocused, setIsFocused] = useState(defaultFocused);

  useLayoutEffect(() => {
    if (!ref.current) {
      return;
    }
    const onFocus = () => setIsFocused(true);
    const onBlur = () => setIsFocused(false);
    if (isFocused) {
      ref.current.focus();
    }

    ref.current.addEventListener("focus", onFocus);
    ref.current.addEventListener("blur", onBlur);

    return () => {
      ref.current.removeEventListener("focus", onFocus);
      ref.current.removeEventListener("blur", onBlur);
    };
  }, [isFocused]);

  return { ref, isFocused, setIsFocused };
};

focus는 이벤트는 말 그대로 focus될때 trigger되는 이벤트이고 blur는 focus가 해제될때 뜨는 이벤틥니다.

그리고 useLayoutEffect에서 clean-up으로 이벤트리스너들을 제거해주면 코드는 완성입니다.

useFocus 사용법

function App() {
  const { ref, isFocused, setIsFocused } = useFocus(false);

  return (
    <>
      <div className={`app ${isFocused && "is-focused"}`}>
        <input type="text" ref={ref} placeholder="focus on me" />
        <span className="tip">포커스 되면 뜨는 텍스트임!!</span>
      </div>
      <button
        onClick={() => {
          setIsFocused(true);
        }}
      >
        포커스 가라
      </button>
    </>
  );
}

defaultFocus를 false로 줬으니 마운트되자마자 focus는 안됩니다.

그래서 input를 클릭하거나 button 이벤트로 focus를 주는 예시로 가져왔습니다. ( 타입스크립트로는 알아서 변환하십쇼 )

useFocus 실행코드

 

React useFocus (forked) - CodeSandbox

hooks.guide example usage

codesandbox.io

동작 및 코드 확인하러 가시면 됩니다.

 

마치며

focus의 기본 예시부터 react에서 적용하는 방법, 그리고 더 나아가 커스텀훅으로 빼서 사용하는 방법까지 알아봤습니다.

간단하고 유용한 커스텀훅인 만큼 인터넷에 다양한 예시와 라이브러리화가 되어있는 훅이기도 합니다.

때론 만들거나 때론 복붙해서 사용하시면 되겠습니다.

이제 모든 컴포넌트에 focus 조지러 가봅시다.

끝.

댓글