Frontend

[React.js] 입력창 입력 시 Focus가 풀리는 현상 해결하기

사과만쥬 2024. 7. 22. 19:33

이 문제는 사실 단순한 문제입다. 그러나 프로젝트 당시에는 이해하지 못했던 부분이라서 다시는 이런 실수를 하지 않기 위해 적었습니다.

 

 

1. 서론


SSAFY 첫 번째 프로젝트를 하던 도중, 한 글자 한 글자 입력할 때마다 focus가 풀리는 현상이 발생하였습니다.

당시 수많은 챗 지피티와 구글링을 통해서 해결방안을 찾으려고 애썼지만 찾지 못했습니다...

 

처음에는 다른 (좀 더 친분이 있는) 백엔드 전문 코치님께 여쭤봤는데, 다른 캠퍼스 코치님이다 보니 본인 반 프로젝트를 신경쓰는데 바쁘시기도 했고, 백엔드가 아닌 프론트엔드라 즉각적인 답변이 어려웠습니다.

그 때 그래도 이게 focus가 풀리는 현상이구나... 정도는 알려주셨습니다. 감사합니다

 

그리고 나서도 해결이 되지 않아 프론트 코치님께 여쭤봤습니다.

 

 

 

당시에는 모든 css 파일을 styled components의 형태로 css와 프론트 로직을 한 파일에 넣어서 작업했는데, 코치님의 답변은 단순히 css 파일과 프론트 로직 파일을 분리해서 프론트 로직 파일에 css 파일을 import시켜라 였습니다.

 

 

근데 코드 길이를 보면 사실 분리하는게 맞잖아? 단순한 파일인데 267줄은 대체 어느나라 코드냐고

 

 

프로젝트 마지막 날이었기 때문에, 당시에는 원인을 찾을 생각은 안 하고 일단 오류 해결하기에 바빴습니다.

그리고 나서, 프론트를 다시 꺼내면서 이게 왜 그렇게 됐는지에 대해 좀 더 자세히 알 수 있었습니다.

제가 리액트의 렌더링에 대해서 잘 이해하지 못해서였다는 것을요...

 

 

2. 문제의 원인

한 줄로 요약하면,

처음에 작업했던 방식(css와 프론트 로직을 한 파일에 작업했던 방식)은 글자를 입력할 때마다 렌더링이 발생했고, 나중 방식(css와 프론트 로직을 별도의 파일에서 작업했던 방식)은 글자를 입력할 때마다 렌더링이 발생하지 않아서 focus가 풀리지 않았던 것이었습니다.

 

 

리액트의 렌더링 단계에 대해서 하나하나 살펴보도록 하겠습니다.

리액트의 렌더링 단계에서 가장 중요하게 기억해야 할 사실은

 

 

React는 기본적으로 부모 컴포넌트가 렌더링되면, 그 안에 있는 모든 자식 컴포넌트를 재귀적으로 렌더링한다는 점

 

 

입니다.

 

또한

 

 

일반적인 렌더링 과정에서, React는 "Props가 변경되었는지 여부"는 신경쓰지 않습니다. 그저 부모 컴포넌트가 렌더링되었기 때문에 자식 컴포넌트도 무조건 렌더링하는 것

 

 

 

라는 점도 주목해 주시면 되겠습니다.

 

 

이 두 가지를 가지고 제가 작업했던 코드를 분석해 보면, 

(코드가 200줄이 넘는 관계로 중요하지 않은 코드들은 다 지웠습니다.)

 

1) useCallback 함수

지금부터 처음에 작업했던 방식의 코드(css와 프론트 로직이 분리되지 않은 방식)를 코드1이라고, 이후에 작업했던 방식의 코드(css와 프론트 로직을 분리한 방식)를 코드2라고 칭하겠습니다.

 

코드 1과 코드 2의 차이는 useCallback 함수의 유무입니다.

function Login() {
  const [loginId, setLoginId] = useState("");
  const [password, setPassword] = useState("");

  const handleLogin = useCallback(() => {
    const headers = {
      Authorization: token,
    };

    const body = {
      loginId: loginId,
      password: password,
    };

    axios
      .post(`${API.LOGIN}`, body, { headers })
      .then((res) => {
        alert("로그인에 성공했습니다.");
        window.location.href = "/";
      })
      .catch((err) => {
        console.log(err);
        alert("로그인에 실패했습니다.");
      });
  }, [loginId, password]);

위는 코드2의 로그인 로직입니다.

 

function Login() {
  // 백-프론트 통신 로직 구현
  const [loginId, setLoginId] = useState("");
  const [password, setPassword] = useState("");

  const handleLogin = () => {
    const headers = {
      Authorization: token,
    };

    const body = {
      loginId: loginId,
      password: password,
    };

    axios
      .post(`${API.LOGIN}`, body, { headers })
      .then((res) => {
        alert("로그인에 성공했습니다.");
        window.location.href = "/";
      })
      .catch((err) => {
        console.log(err);
        alert("로그인에 실패했습니다.");
      });
  };

위는 코드1의 로그인 로직입니다.

여기서 차이가 하나 발생하는데, 보이시나요?

 

코드2의 handleLogin

 

코드1의 handleLogin

 

코드2는 useCallback 함수를 이용해서 작업하였고, 코드1은 그렇지 않았습니다.

코드2의 useCallback함수는 loginId와 password가 변경되지 않는 한, handleLogin 함수가 동일한 참조를 유지하게 하는 기능을 하고 있습니다. 이로 인해 불필요한 리렌더링을 방지할 수 있습니다.

 

그러나 코드 1의 경우에는 함수가 매번 새롭게 정의되어 리렌더링이 발생합니다.

 

2) 잘 모듈화된 css(명확하지 않음)

코드 2는 module.css파일을 import 시켜서 사용하였습니다.

import styles from "./login.module.css"; // CSS 모듈 import

 

이처럼 모듈화된 css 파일을 이용함으로써 컴포넌트가 상태 변화를 통해 렌더링될 때 각 요소의 스타일이 일관되게 적용될 수 있도록 합니다.

 

코드 자체도 상당히 스파게티 코드이긴 합니다.

 

위의 코드는 코드1의 css 일부인데, styled component를 사용하였습니다.

이 styled component의 경우에는 React 컴포넌트가 렌더링 될 때, styled-component가 렌더링됩니다. 우리가 알고 있는 리액트 컴포넌트의 렌더링 과정과 동일합니다. 이 렌더링 과정에서 불필요한 렌더링이 더 생겨서 문제된 것으로 보입니다.

이 부분은 추후에 프론트를 다시 하게 된다면, 공부할 필요가 있어 보입니다.

 

 


참고

https://velog.io/@arthur/%EB%B2%88%EC%97%AD-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%8F%99%EC%9E%91%EC%9D%98-%EA%B1%B0%EC%9D%98-%EC%99%84%EB%B2%BD%ED%95%9C-%EA%B0%80%EC%9D%B4%EB%93%9C-A-Mostly-Complete-Guide-to-React-Rendering-Behavior