[2회차 발표] 이벤트에 응답하기, state: 컴포넌트의 메모리

Sheryl Yun·2023년 10월 2일
0

리액트 공식 문서 '상호작용 추가하기' 파트
1번째, 2번째 섹션 발표 자료 정리

상호작용 추가하기 - 서론

  • 화면은 사용자 입력(상호작용)에 따라 갱신된다.
    • 예: 이미지 클릭 시 선택된 이미지 표시하기
  • State는 시간이 지남에 따라 변화하는 데이터
    • Props는? - 불변하는 데이터
  • 모든 리액트 컴포넌트에는 state 추가 및 변경이 가능하다.

이벤트에 응답하기

  • React의 JSX 리턴문에 이벤트 핸들러 추가 가능
    • 예: onClick, onChange, input 포커스 등

이벤트 핸들러
사용자의 상호작용에 반응하여 자체적으로 발생하는 이벤트를 다루는 함수

이벤트 핸들러 추가하는 법

  1. 컴포넌트 함수의 return문 위쪽에 있는 자바스크립트 영역에 이벤트 핸들러 함수를 정의한다. (예: const handleChange = () => ~)
  2. JSX 태그의 props로 이벤트 핸들러 함수를 전달한다. (예: <input onChange={handleChange} />)

이벤트 핸들러 함수 네이밍 관습
앞에 handle을 붙인다. (예: handleClick, handleMouseEnter 등)

Pitfall: 함정 🥅

  • 이벤트 핸들러 함수는 즉시 호출되는 게 아니라 '전달'되어야 한다.
    • <button onClick={handleClick}> (올바른 형태)
    • <button onClick={handleClick()}> (틀린 형태)
  • 이벤트 핸들러 함수의 내부 로직을 인라인으로 전달하려고 할 때는 익명 함수로 호출한다.
    • <button onClick={() => console.log('handleClick 실행!')}> (올바른 형태)
    • <button onClick={console.log('handleClick 실행!')}> (틀린 형태)
// 위 handleClick 함수의 생김새

const handleClick = () => {
	console.log('handleClick 실행!');
} 

이벤트 핸들러를 props로 전달하기

  • 보통 자식 컴포넌트에서 사용할 이벤트 핸들러 함수는 부모 컴포넌트 내부에 지정
    • 이벤트 핸들러에서 업데이트되는 state(useState)가 부모에 있어야 단방향 데이터 흐름을 가진 리액트에서 데이터 전달에 더 용이하기 때문

이벤트 핸들러 네이밍 관습

  • 이벤트 핸들러는 JSX에 props로 들어가는 속성이고, 이벤트 핸들러 함수는 이 props의 중괄호 안에 들어가는 함수를 의미한다.
  • 이벤트 핸들러는 관습적으로 on으로 시작하고 이후 카멜 케이스로 작성한다.

BONUS: 다양한 변수명 케이스

  • 카멜 케이스: 맨 앞 소문자 + 단어 단위로 첫 글자를 대문자로 (예: onChange, handleChange)
  • 케밥 케이스: 모두 소문자 + 하이픈으로 연결 (예: profile-image.png, /api/main/sales-mode)
  • 파스칼 케이스: 맨 앞이 대문자 (예: const Sidebar = () => ...)
  • 스네이크 케이스: 모두 소문자 또는 대문자, 언더바로 연결 (예: CARD_INFO)

이벤트 전파 (Propagation)

  • 하위 컴포넌트에서 발생한 이벤트는 기본적으로 상위로 전파 (= 버블링)
  const handleToolbarClick = () => console.log('Toolbar 클릭!');
  const handleButtonClick = () => console.log('Button 클릭!');
  
  <Toolbar onClick={handleToolbarClick}>
  	<Button onClick={handleButtonClick}>클릭</Button>
  </Toolbar>
  • 하위 컴포넌트인 Button을 클릭하면 콘솔에 'Button 클릭!'이 출력된 뒤 상위 컴포넌트 Toolbar에 있는 'Toolbar 클릭!' 콘솔도 실행됨
    => 의도된 행위는 하위 컴포넌트의 콘솔만 출력하는 것
  • 하위에서 발생한 이벤트가 부모 이벤트 핸들러까지 건드려서 발생하는 현상
    => 해결: 하위 컴포넌트의 이벤트 핸들러 함수에 e.stopPropagation()를 선언하여 버블링을 막는다.
    • e.stopPropagation(): e(event) 객체의 메서드 ('전파를 멈추라'는 뜻)

이벤트 전파가 발생하는 이유?

브라우저에서는 기본적으로 이벤트 버블링(전파)이 발생한다.
그러면 이러한 이벤트 버블링은 왜 발생하는 것일까?
(ChatGPT를 통해 추가 자료 조사)

요약: 이벤트 위임을 위해서이다.

  • 이벤트 위임이란 상위 DOM에 이벤트를 등록하면 하위 DOM에 일일이 이벤트를 등록하지 않아도 되는 것을 의미한다. 하위에서 이벤트가 발생해도 상위까지 버블링되어 올라가기 때문에 가능한 일이다.
  • 이벤트 위임이 필요하지 않은 경우라면 e.stopPropagation()으로 이벤트 버블링을 막아주면 된다.
    • 이렇게 이벤트 버블링이 필요하지 않은 경우
      예: Card 컴포넌트 안에 Like(좋아요) 하트 컴포넌트가 있는데 하트 컴포넌트만 눌리게 하고 싶은 경우

  • 상위 Card로 올라가는 이벤트 전파를 막아서 하위 컴포넌트인 하트를 눌러도 Card 컴포넌트 이벤트(예: 상세 페이지로 이동)가 동작하지 않고 하트의 좋아요 이벤트만 발생

Pitfall: 함정 🥅

React에서 onScroll을 제외한 모든 이벤트는 전파된다.
추가: onScroll 외에 인풋 focus 이벤트도 전파되지 않는다.

전파 멈추기

  • 이벤트 핸들러 함수는 유일하게 이벤트 객체(e 또는 event)만 인자로 받음
    • 이벤트 객체(e)를 사용하여 이벤트에 대한 정보를 읽거나 제공되는 메서드로 전파 중지(e.stopPropagation()) 또는 브라우저 기본 동작 중지(e.preventDefault()) 등이 가능

Deep Dive 🌊

캡쳐링(capturing) 동작

  • 캡쳐링은 버블링과 반대로 상위에서 하위로 이벤트가 전파되는 현상이다.
  • onClickCapture를 이벤트 핸들러로 넣어주면 의도적으로 캡쳐링 이벤트를 일으킬 수 있다.
  • 하지만 거의 안 씀 (라우터나 분석에 잠깐 쓰는 정도)

전파의 대안으로 핸들러 전달하기

  • 공식 문서 제목이 약간 애매한 감이 있는데, 아마 stopPropagation으로 전파를 막은 뒤 다른 동작을 추가할 수 있다는 점에서 '대안'이라고 표현한 것 같다.
function Button({ onClick, children }) {
	return (
    	<button onClick={(e) => {
        	e.stopPropagation();
            onClick();
        }}>
        	{children}
        </button>
    )
}
  • 하위 컴포넌트인 Button이 부모로부터 onClick 이벤트 핸들러 함수와 children을 받는 형태이다.
  • 전파는 자동으로 일어나며 e.stopPropation()으로 막을 수 있다.
  • 전파를 막은 뒤에 개발자가 의도적으로 추가로 실행할 동작을 지정할 수 있다.
  • 이렇게 하면 전파를 막은 것과 막은 후에 실행되는 코드의 흐름을 파악하기 쉽다.

브라우저 기본 동작 방지하기

  • e.preventDefault(): 기본 브라우저 동작이 있을 때 사용하면 기본 동작을 막는다.
  • form의 submit 이벤트의 기본 동작인 새로고침을 막을 때 많이 사용하고, 그 외 input checkbox의 클릭 시 체크되는 기본 동작 등을 막을 수 있다.

이벤트 핸들러의 side effect는 필연적

  • '순수하다': 인풋이 같으면 아웃풋이 같은 성질
  • 렌더링 함수와 달리 이벤트 핸들러는 순수할 필요가 없다.
    • 타이핑에 대한 응답으로 input 값 변경, 버튼 클릭에 따라 목록 변경 등 인풋에 맞춰 아웃풋이 유지될 필요 없고 변경이 자주 일어남
  • 이 동작을 위해 기존의 상태를 저장해둘 곳이 필요
    • React에서는 컴포넌트의 메모리인 state를 사용해 이 작업을 수행한다.

컴포넌트의 메모리: State

  • 리액트에서 컴포넌트는 사용자의 상호작용의 결과로 화면을 변경
    • 예: form 입력 시 인풋 업데이터, 이미지 캐러셀에서 '다음' 클릭 시 보여지는 이미지 변경, 쇼핑몰에서 '구매하기'를 누르면 장바구니에 제품이 담김
  • 이렇게 하려면 기존의 인풋 값, 기존의 이미지, 기존의 장바구니 내용을 '기억'하고 있어야
    => 이를 위한 컴포넌트 내부에 있는 메모리를 state라고 부름

일반 변수로는 충분치 않은 경우

  • 컴포넌트 내에서 state를 일반 지역 변수로 관리하면 2가지 문제가 발생한다. (지역 변수란, 해당 함수가 가지고 있는 변수)
    • 컴포넌트 함수가 리렌더링될 때 지역 변수는 유지되지 않는다. (매번 새로 선언됨)
    • 지역 변수가 재할당으로 인해 변경되었을 때 이 사실을 리액트가 인지하지 못해서 상태가 변경되어도 화면이 리렌더링되지 않는다.
import { sculptureList } from './data.js';

export default function Gallery() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }

  let sculpture = sculptureList[index];
  
  return (
  	... 
  );
  
  // '다음'을 눌러 handleClick이 호출되고 index 변수가 바뀌어도 
  // 이미지가 변경(= 화면이 리렌더링)되지 않는다.

=> 이 두 가지를 모두 해결해주는 것이 리액트의 hook 중 하나인 useState

  • 상태 값을 기억해두고,
  • 상태가 변경되었을 때 화면이 리렌더링된다.

useState 형태

  • useState는 함수여서 인자를 받을 수 있다.
    • 이 인자로 상태 변수의 초기값을 받는다.
  • useState에서 반환된 값을 배열 비구조화 할당으로 꺼낸다.
// 원래 반환되는 형태
const result = useState(0);

// 반환된 값을 배열 비구조화 할당으로 꺼냄 (= 보통 사용하는 useState)
const [index, setIndex] = useState(0);

배열 비구조화 할당 vs. 객체 비구조화 할당

  • 배열 비구조화 할당
    • 배열을 사용하여 변수를 할당했을 때 각 요소를 사용자 지정 변수로 꺼내서 출력할 수 있다.
    • 객체는 key로 정해져 있기 때문에 이 부분이 힘들다.
      => useState에서 배열 비구조화 할당을 쓰는 이유 (사용자가 마음대로 변수를 지정해서 꺼내 쓸 수 있다)
const numbers = [1, 2, 3];
const [a, b, c] = numbers;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
  • 객체 비구조화 할당
    • 변수에 객체를 할당하고 객체의 key를 중괄호 안에 꺼내서 value 출력에 사용한다.
const person = { name: 'John', age: 30 };
const { name, age } = person;
console.log(name); // 'John'
console.log(age);  // 30

여러 state 변수 지정하기

  • 공식 문서의 이미지 넘기기 예제에서 index는 현재 보여주는 이미지의 인덱스(number)이고 showMore는 해당 이미지의 상세 detail을 보여줄 것인지 여부(boolean)이다.
  • 이 둘처럼 서로 연관이 없는 경우 state 변수를 각각 따로 선언하는 것이 좋다.
  • 하지만 여러 개의 state 변수를 자주 함께 변경하는 경우에는 하나의 useState 객체로 선언하는 것이 더 좋다.
    • 예: 인풋 필드가 많은 form의 경우 인풋 별로 매번 state 변수를 선언하는 것보다 하나의 객체에 모으고 spread 연산자로 변경하는 것이 더 편함
    • 성능 면에서도 변수 선언을 덜 하는 장점

🌊 Deep Dive: 어떤 state를 반환할지 리액트가 어떻게 인식할까?

  • 따로 식별자가 있는 것은 아니고 hook이 호출되는 순서에 의존한다.
    • 최상위 수준에서만 hook을 호출하면 항상 같은 순서로 호출되어 문제 없이 작동
      => 최상위 수준에서만 hook을 호출해야 하는 이유!

state의 또 다른 특징: isolated and private

  • state는 컴포넌트 함수 내부에 완전히 지역적으로 존재한다.
    • 다른 컴포넌트 함수 내부에 동일한 이름의 state를 선언해도 서로 영향을 미치지 않는다.
    • private: 다른 인스턴스가 전혀 알 수 없음

=> 모듈 상단에 선언된 일반 변수와의 차이점이라고 할 수 있다.

  • 두 컴포넌트 간의 state를 동기화(서로 공유)하려면?
    • 각각 따로 선언된 state를 각 컴포넌트에서 제거한 뒤, 두 컴포넌트를 모두 포함하는 공통된 부모 컴포넌트로 state를 끌어 올린다.

추가: 리액트 Hook

(ChatGPT로 추가 자료 조사)

  • Hook은 리액트의 컴포넌트 함수가 리액트 기능을 사용할 수 있게 해주는 특수한 함수이다.

Hook이 지켜야 하는 규칙

  • 네이밍이 'use'로 시작해야 리액트가 hook으로 인지한다.
    • 커스텀 hook이든, 리액트에서 제공하는 hook이든 상관 없이
  • 컴포넌트 함수 내 최상위 레벨에 선언되어야 한다.
    • 최상위 파일 index.js라는 뜻이 아니라 컴포넌트 함수 내에서의 최상위
      = 함수나 조건문, 반복문 같은 중괄호 내에서 호출되면 안 된다
  • 일반 자바스크립트 함수가 아닌 리액트 컴포넌트 함수 내에서만 호출되어야 한다.
  • 선언된 순서에 영향 받는다.
    • 재료인 useState 먼저 선언 후 useEffect를 선언
    • useEffect가 한 컴포넌트 내에서 여러 개인 경우에 순서를 고려하지 않으면 버그 발생
profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글