본문 바로가기

WINK-(Web & App)/React.js 스터디

[2025 1학기 React.js 스터디] 이서준 #4주차

반응형

시험끝난지얼마안됏는데요

시작하겠습니다.

useRef란 무엇인가

JS에서는 특정 DOM을 선택해야 하는 상황에 getElementById, querySelector 같은 DOM Selector 함수를 사용해서 DOM을 선택한다.

React를 사용하는 프로젝트에서도 DOM을 직접 선택해야 하는 상황이 발생할 때도 있는데, 예를 들어 특정 엘리먼트의 크기를 가져와야 한다던지, 스크롤바 위치를 가져오거나 설정해야한다던지, 또는 포커스를 설정해줘야한다던지 등 다양한 상황이 있다. 또한 외부 라이브러리를 사용해야 할 때도 특정 DOM에다 적용하기 때문에 DOM을 선택해야 하는 상황이 발생할 수 있다.

이럴 때 React에서는 ref라는 것을 사용한다.

함수형 컴포넌트에서 ref를 사용할 때는 useRef라는 Hook 함수를 사용한다. 클래스형 컴포넌트에서는 콜백 함수를 사용하거나 React.createRef 라는 함수를 사용하는데 이건 나중에 알아보도록 하자.

일단 inputSample.js의 코드는 아래와 같다.

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

function InputSample() {
  const [inputs, setInputs] = useState({
    name: "",
    nickname: "",
  });
  const nameInput = useRef();

  const { name, nickname } = inputs;

  const onChange = (e) => {
    const { value, name } = e.target;
    setInputs({
      ...inputs,
      [name]: value,
    });
  };

  const onReset = () => {
    setInputs({
      name: "",
      nickname: "",
    });
    nameInput.current.focus();
  };
  return (
    <div>
      <input
        name="name"
        placeholder="이름"
        onChange={onChange}
        value={name}
        ref={nameInput}
      />
      <input
        name="nickname"
        placeholder="닉네임"
        onChange={onChange}
        value={nickname}
      />
      <button onClick={onReset}>초기화</button>
      <div>
        <b>값: </b>
        {name} ({nickname})
      </div>
    </div>
  );
}

export default InputSample;

전에 만들었던 InputSample에서는 초기화 버튼을 누르면 포커스가 초기화 버튼에 그대로 남아있게 된다. useRef를 사용해서 이름에 input에 포커스가 잡히도록 구현해본 코드가 위의 코드이다.

useRef()를 사용하여 Ref 객체를 만들고, 이 객체를 우리가 선택하고 싶은 DOM에 ref 값으로 설정해 주어야 한다. 그러면 Ref 객체의 .current 값은 우리가 원하는 DOM을 가리키게 된다.

→ onReset 함수에서 focus() DOM API를 호출해서 input에 포커스를 함

배열 렌더링하기

배열 렌더링 하는 방법

const users = [
  {
    id: 1,
    username: 'velopert',
    email: 'public.velopert@gmail.com'
  },
  {
    id: 2,
    username: 'tester',
    email: 'tester@example.com'
  },
  {
    id: 3,
    username: 'liz',
    email: 'liz@example.com'
  }
];

배열이 이렇게 있다고 가정했을 때, 내용을 컴포넌트를 렌더링 하기 위해서는 두 가지 키워드만 기억하면 될 것 같다. → 1. 함수 선언 2. map 함수 사용

코드

import React from "react";

function User({ user }) {
  return (
    <div>
      <b>{user.username}</b> <span>({user.email})</span>
    </div>
  );
}

function UserList() {
  const users = [
    {
      id: 1,
      username: "velopert",
      email: "public.velopert@gmail.com",
    },
    {
      id: 2,
      username: "tester",
      email: "tester@example.com",
    },
    {
      id: 3,
      username: "liz",
      email: "liz@example.com",
    },
  ];

  return (
    <div>
      {users.map((user) => (
        <User user={user} />
      ))}
    </div>
  );
}
export default UserList;

{ user }는 객체 구조 분해 할당 인데, function User(props) 라고 쓰는 대신, function User({user})라고 쓰면 props.user를 바로 꺼내 쓰는 것과 같다.

근데 그냥 이렇게 해버리면, 콘솔에서 에러가 발생하는데, 리액트에서는 배열을 렌더링 할 때 key라는 props를 설정해야 한다. key 값은 각 원소들마다 가지고 있는 고유값으로 설정을 해야 하는데, 이 경우엔 id가 고유값이다.

따라서 코드의 return 부분을 이렇게 수정해 주어야 한다.

return (
    <div>
      {users.map(user => (
        <User user={user} key={user.id} />
      ))}
    </div>
  );

만약에 배열 안의 원소가 가지고 있는 고유한 값이 없다면 map() 함수를 사용 할 때 설정하는 콜백함수의 두 번째 파라미터 index를 key로 사용하면 된다.

<div>
  {users.map((user, index) => (
    <User user={user} key={index} />
  ))}
</div>

key 값이 필요한 이유

React에서 key 값이 필요한 이유는 리스트 렌더링 시 성능 최적화와 정확한 UI 업데이트 위해서이다.

  1. 요소 구분을 위해
  2. key가 없으면 React는 DOM 업데이트 시 비교 기준이 없어 전체를 다시 렌더링하거나 잘못된 업데이트를 할 수 있다.
  3. 불필요한 DOM 작업을 줄여 성능이 향상된다.
  4. 동일 컴포넌트 재사용하는 경우 상태(state)가 잘못 유지될 수 있다. (input 요소가 여러개 있을 때 입력 값이 꼬일 수 있다.)

useRef로 컴포넌트 안의 변수 만들기

useRef는 DOM을 선택하는 용도 외에도, 다른 용도가 한가지 더 있다.

컴포넌트 안에서 조회 및 수정할 수 있는 변수를 관리하는 것이다.

useRef로 관리하는 변수는 값이 바뀐다고 해서 컴포넌트가 리렌더링되지 않는다.

리액트 컴포넌트에서의 상태는 상태를 바꾸는 함수를 호출하고 나서 그 다음 렌더링 이후로 업데이트 된 상태를 조회할 수 있는 반면, useRef로 관리하고 있는 변수는 설정 후 바로 조회할 수 있다.

이 변수를 사용하여 다음과 같은 값을 관리 할 수 있습니다.

  • setTimeout, setInterval 을 통해서 만들어진 id
  • 외부 라이브러리를 사용하여 생성된 인스턴스
  • scroll 위치

useRef()를 사용 할 때 파라미터를 넣어주면, 이 값이 .current 값의 기본값이 되고,

이 값을 수정할 때에는 .current 값을 수정하면 되고 조회할 때는 .current를 조회하면 된다.

const nextId = useRef(4); // 선언 -> nextId는 ref 객체라 요소 수정이 가능
nextId.current += 1; // 수정
nextId.current // 조회

여기서 의문점이 들었다. useRef와 useState의 확실한 차이점이 무엇인가?

useRef란?

컴포넌트는 자신의 상태값이 변경되거나, 부모로부터 받은 인자값이 변경되었을 때 새로 렌더링된다.

useRef는 DOM 요소에 직접적으로 접근할 때(ex: focus, scroll, video, controls)도 사용하지만,

렌더링과 관계없이 특정 값을 유지해야 할 때 (ex: 이전 값 저장, 타이머 ID 저장 등), 렌더링을 발생시키지 않고 상태를 변경하고 싶을 때 사용한다.

즉, 컴포넌트 내의 변수값을 조회, 수정하는 용도로 사용할 수 있다.

useRef 주요 특징

  • 렌더링 없이 값을 유지
  • DOM 요소에 직접 접근 가능 (input, button 등)
  • 컴포넌트가 리렌더링 되어도 값이 초기화되지 않음

Ref는 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법을 제공한다.

사용법

ref를 이용하여 값 참조하기

컴포넌트의 최상위 레벨에서 useRef를 호출하여 하나 이상의 ref를 선언한다.

import { useRef } from 'react';

function Stopwatch() {
  const intervalRef = useRef(0);
  }

useRef는 초기값으로 설정된 단일 current 프로퍼티가 있는 ref 객체를 반환한다.

다음 렌더링에서 useRef는 동일한 객체를 반환한다. 정보를 저장하고, 나중에 읽을 수 있도록 current 속성을 변경할 수 있다.

→ state와 비슷하지만, 둘 사이에는 중요한 차이점이 있다.

ref를 변경해도 리렌더링을 촉발하지 않는다! - ref는 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는데 적합하다.

  • interval ID를 저장했다가, 나중에 불러와야하는 경우 ref에 넣을 수 있다.
function handleStartClick() {
  const intervalId = setInterval(() => {
    // ...
  }, 1000);
  intervalRef.current = intervalId;
}

function handleStopClick() {
  const intervalId = intervalRef.current;
  clearInterval(intervalId);
}
  • setInterval()
    > const intervalId = setInterval(() => console.log(new Date()), 2000);
    < Sun Dec 12 2021 12:45:31 GMT-0500 (Eastern Standard Time)
    < Sun Dec 12 2021 12:45:33 GMT-0500 (Eastern Standard Time)
    < Sun Dec 12 2021 12:45:35 GMT-0500 (Eastern Standard Time)
    > clearInterval(intervalId);
    
    setInterval() 함수를 사용한 후에는 반드시 clearInterval() 함수를 사용해서 타이머를 청소해주는 습관을 들여야 한다.
  • useEffect cleanup 부분 정리 참조
  • setInterval() 함수는 인터벌 아이디(Interval ID)라고 불리는 숫자를 반환하는데, 인터벌 아이디는 setInterval() 함수를 호출할 때마다 내부적으로 생성되는 타이머 객체를 가리키고 있다. 이 값을 인자로 clearInterval()함수를 호출하면 코드가 주기적으로 실행되는 것을 중단시킬 수 있다.

ref를 사용하면 보장되는 것들

  • (렌더링할 때마다 재설정되는 일반 변수와 달리)리렌더링 사이에 정보를 저장할 수 있다.
    • 일반 변수는 리렌더링되면 값이 초기화된다.
    • ref는 리렌더링되더라도 값을 유지할 수 있다.
    • 코드 실행 결과리렌더링 후 상태변수 값
      normalVar 0 (리렌더링되며 초기화됨)
      refVar.current 1 (리렌더링 후에도 유지됨)
      count 1 (state는 리렌더링을 트리거했으므로 업데이트됨)
    • import { useState, useRef } from "react"; function Counter() { let normalVar = 0; // 일반 변수 const refVar = useRef(0); // ref 변수 const [count, setCount] = useState(0); const handleClick = () => { normalVar++; // 리렌더링되면 초기화됨 refVar.current++; // 리렌더링 후에도 값 유지됨 setCount(count + 1); // 리렌더링 발생 }; console.log("컴포넌트 렌더링됨!"); return ( <div> <p>일반 변수: {normalVar}</p> <p>useRef 변수: {refVar.current}</p> <p>state 변수: {count}</p> <button onClick={handleClick}>클릭</button> </div> ); }
  • useRef를 이용하여 DOM 요소에 접근 가능하다.
    • useRef를 사용하면 특정 HTML 요소를 직접 조작할 수 있다.
    • document.querySelector를 쓰지 않고도 특정 DOM요소를 쉽게 가져올 수 있다.(장점)
    • useRef로 input에 포커스 주기disabled는 DOM 속성 중 하나이기 때문에 inputRef.current.disabled와 같이 참조된 DOM 요소에 직접적으로 적용되어야 한다.
    • const ListItem = () => { const inputRef = useRef<HTMLInputElement>(null);//ts //js: const inputRef = useRef(null); function clickModifyBtn() { if (inputRef.current !== null) { inputRef.current.disabled = false; //input 비활성화 해제 inputRef.current.focus(); //input에 focus } } return ( <input ref={inputRef}/> //todo input <button onClick={clickModifyBtn}>수정하기<button/> //수정 버튼 ) }
  • (리렌더링을 촉발하는 state 변수와 달리) 변경해도 리렌더링을 촉발하지 않는다.
  • (정보가 공유되는 외부 변수와 달리) 각각의 컴포넌트에 로컬로 저장된다.
  • ex ) ID / Password ⇒ ID는 useRef ( 리렌더링이 필요 없음 ) / Password ( 입력할 때마다 양식에 맞는지 확인해야함 )

주의할 점

렌더링 중에는 ref.current를 쓰거나 읽으면 안됨!

React는 컴포넌트의 본문이 순수 함수처럼 동작하기를 기대한다.

대신 이벤트 핸들러나 Effect에서 ref를 읽거나 쓸 수 있습니다.

function MyComponent() {
  // ...
  useEffect(() => {
    myRef.current = 123;
  });
  // ...
  function handleClick() {
    doSomething(myOtherRef.current);
  }
  // ...
}

useState와 useRef 비교

쉽게 말해서

  • 상태가 바뀔 때마다 리렌더링이 필요함 → useState 사용! ( 비밀번호 확인 )
  • 리렌더링이 필요하지 않은 경우 → useRef 사용! ( ID 입력 )

배열에 항목 추가하기

배열에 항목을 추가하는 방법은, 두가지가 있다.

1. spread 연산자 사용하기

 const onCreate = () => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers([...users, user]);

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };

이렇게 하면, …users를 이용해 users 배열을 복사한 후에, 복사본에 user 객체를 추가해준다.

2. concat 함수 사용

setUsers(users.concat(user)); // 얘도 users 배열을 복사해서 고치는 것이라 불변성 유지

배열에 항목 제거하기

User 컴포넌트의 삭제 버튼이 클릭 될 때는 user.id 값을 앞으로 props로 받아올 onRemove 함수의 파라미터로 넣어서 호출해줘야 한다.

onRemove는 “id가 __인 객체를 삭제해라”라는 역할을 가지고 있다.

이 onRemove 함수는 UserList에서도 전달 받을 것이며, 이를 그대로 User 컴포넌트에게 전달해준다.

( 둘 다 매개변수로 넣어주면 된다)

onRemove 함수에서 불변성을 지키면서 특정 원소를 배열에서 제거하기 위해서는 filter 배열 내장 함수를 사용하는 것이 가장 편하다. 이 함수는 배열에서 특정 조건이 만족하는 원소들만 추출하여 새로운 배열을 만들어준다.

const onRemove = id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users.filter(user => user.id !== id));
  };

배열 항목 수정하기

User 컴포넌트에 계정명을 클릭했을 때, 색상이 초록색으로 바뀌고, 다시 누르면 검정색으로 바뀌도록 구현했다.

function User({ user, onRemove, onToggle }) {
  return (
    <div>
      <b
        style={{
          cursor: 'pointer',
          color: user.active ? 'green' : 'black'
        }}
        onClick={() => onToggle(user.id)}
      >
        {user.username}
      </b>

      <span>({user.email})</span>
      <button onClick={() => onRemove(user.id)}>삭제</button>
    </div>
  );
}
function UserList({ users, onRemove, onToggle }) {
  return (
    <div>
      {users.map(user => (
        <User
          user={user}
          key={user.id}
          onRemove={onRemove}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
}

style을 사용해서 커서 모양과 색깔을 바꾸는 것을 만들었다.

그 다음 App.js에서 onToggle 함수를 구현했는데, 배열의 불변성을 유지하며 배열을 업데이트 할 때에도 map 함수를 사용할 수 있다.

const onToggle = id => {
    setUsers(
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  };

만약에 클릭한 것의 id가 user.id와 같으면 active를 True면 False로, False면 True로 바꿔준다.

만약에 같지 않다면, 그냥 그대로 유지한다.

useEffect

useEffect는 리액트 함수형 컴포넌트에서 side effects를 처리하는 Hook이다.

즉, 컴포넌트가 렌더링될 때 실행해야 하는 코드이다.

(API 호출, 이벤트 리스너 등록, 타이머 설정)

  • Side Effect하지만 Side Effect들은 state, props의 변화가 있을 때마다 렌더링되어 그 로직이 실행된다.그래서 React 에서는 이런 Side Effect를 일으키기 적절한 장소로써 useEffect hook을 제공한다.ㄴ 렌더링에 영향을 주지 않도록 설계되었다!
  • useEffect는 Side Effect를 렌더링 이후에 발생시킨다.
  • 렌더링과 무관한 로직이 렌더링 과정에서 실행되기 때문에, 렌더링 자체에 영향을 줘 성능 상 악영향을 끼칠 수도 있게 된다.
  • 함수가 어떤 동작을 할 때, I/O 이외의 다른 값을 조작한다면, 이 함수에는 Side Effect(부수 효과)가 있다고 표현한다.

useEffect 기본 문법

useEffect(() => {
  // 실행할 코드
}, [의존성 배열]);
  • 첫 번째 인자: 실행할 함수
  • 두 번째 인자: 의존성 배열 ([ ])
    • 빈 배열 → 마운트될 때만 실행( 1번만 )
    • 배열 없음 → 매 렌더링마다 실행
    • 특정 값 ([값]) → 그 값이 변할 때만 실행

특정 값이 변경될 때 실행 (state 감지)

const [count, setCount] = useState(0);

useEffect(() => {
  console.log(`count가 변경됨: ${count}`);
}, [count]);

→ count가 변할 때만 실행됨. (다른 state가 변경되어도 발생 x)

Clean up Effect

Cleanup Effect는 간단하게 설명하자면, 이전에 일으킨 Side Effect를 정리할 필요가 있을 때 사용한다.

  • 메모리 누수를 방지 → 이벤트 리스너, 타이머, 구독 등을 정리해야 함.
  • 불필요한 코드 실행 방지 → 예전 데이터를 그대로 사용하지 않도록 최신 상태 유지.

setInterval 타이머 예제

import { useEffect, useState } from "react";

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount((prev) => prev + 1);
    }, 1000);

    // Cleanup: 컴포넌트가 언마운트될 때 clearInterval 호출!
    return () => {
      clearInterval(interval);
      console.log("⏰ 타이머 정리 완료!");
    };
  }, []);

  return <h1>⏳ {count} 초 경과</h1>;
}

이 코드에 Cleanup 함수가 없으면

  • setInterval이 계속 실행되면서 컴포넌트가 없어져도 타이머가 남아있다.
  • 불필요한 업데이트가 발생하여 앱 성능이 저하될 수 있다.
  • 따라서 언마운트 될 때 clearInterval로 타이머를 정리하는 것이 필수적이다.

상황 Cleanup이 필요한 이유 해결 방법

Firestore 실시간 구독 리스너가 계속 남아 메모리 누수 발생 unsubscribe() 호출
setInterval / setTimeout 컴포넌트가 사라져도 타이머가 실행됨 clearInterval() 또는 clearTimeout() 호출
이벤트 리스너 (addEventListener) 리스너가 중첩되어 성능 저하 removeEventListener() 호출

cleanup 함수는 꼭 필요할 때만 추가하면 되고, 리

소스를 많이 차지하는 기능이 있을 때 사용하면 좋다.

 

끝내겠습니다.

 

진짜 없었습니다

반응형