리액트 useState

hyun·2022년 4월 17일
1

React Hooks

목록 보기
2/5
post-thumbnail

이 포스트에서 다룰 것

리액트에서 함수형 컴포넌트를 사용한다면 훅(Hook)이라는 기능을 필수적으로 사용하게 된다. 지난 포스팅에서도 다루었지만, 예제와 함께 더 자세하게 기본 중의 기본 훅인 useState를 알아볼 것이다.

들어가기 전에

자바스크립트 ES6의 Destructuring Assignment와 Spread syntax 문법을 알아야 이해할 수 있다.

Destructuring Assignment

Destructuring Assignment(구조 분해 할당)이란, 이름 그대로 객체나 배열 등의 구조를 분해해서 할당해주는 문법이다. 이름만 거창하지 코드를 보면 이해가 쉽다.

const person = {
	name : "hyun",
  	job : "developer"
}

const name = person.name;
const job = person.job; 

기존 문법에서 객체의 key,value에 접근하기 위해서는 .을 통해 코드를 작성했어야 했다. Destructuring Assignment를 이용하게 되면 객체의 key를 {} 안에 정의해주고 객체를 넣어주면, 객체의 key와 value들이 각각 할당된다.

const person = {
	name : "hyun",
  	job : "developer"
}

const {name,job} = person;

객체 안의 key의 이름을 그대로 쓰지 않고 바꿀 수도 있다.

const person = {
	name : "hyun",
  	job : "developer"
}

const {name: personName, job: personJob} = person;
console.log(personName, personJob);	// hyun, developer

객체 뿐만 아니라 배열에서도 동일하게 사용이 가능하다.

const people = ["철수", "영희"];

const first = people[0];
const second = people[1];

기존의 배열 원소를 접근하는 방법이다. 이를 Destructuring Assignment를 사용하면 아래처럼 쓸 수 있다.

const people = ["철수", "영희"];

const [first, second] = people;
console.log(first, second); // 철수, 영희

Spread Syntax

Spread Syntax란 배열이나 객체 앞에 ...을 붙여서 펼쳐주는 문법이다. 이 문법 역시 코드를 보면 이해가 쉽다.

const obj1 = { key: 'key1'};
const obj2 = { key: 'key2'};
const array = [obj1, obj2];

객체들을 담고 있는 배열이 있다. 이 배열을 복사하기 위해서는 어떻게 해야 할까?

const copy = [...array];

[]로 새로운 배열을 만들고, array 안에 들어있던 녀석들을 그 안에 ...를 통해 기존의 배열을 벗겨낸 후 알맹이만 가져와 넣는다.

배열을 펼쳐서 새로운 원소를 추가하는 것도 간단하다.

const copy2 = [...array, { key: 'key3' }];

중요한 것은 spread 연산자는 객체 안에 들어있는 item들을 하나하나씩 복사해 오는 것이 아니라, 객체가 가리키고 있는 주소의 참조값만 복사해 온다는 것이다. spread 연산자를 통해 복사해 온다고 해도, 복사 원본을 변경하게 되면 복사한 녀석도 같이 변경이 되기 때문에 주의해야 한다. 배열 뿐만 아니라 객체에도 사용이 가능하다.

State란?

리액트에서 state란 말 그대로 상태, 컴포넌트가 가지는 상태를 말한다. 예를 들어 사람이라는 컴포넌트가 있다면 이름, 나이, 성별 등의 상태를 가질 수 있다. useState는 컴포넌트의 상태를 생성&업데이트 할 수 있는 도구(훅)이다.

useState 사용법

useState를 사용하기 위해서는 반드시 import를 해야 한다.

import { useState } from 'react';

useState는 자바스크립트 ES6의 Destructuring Assignment 문법과 함께 자주 사용한다.

const [state, setState] = useState(초기값);

먼저, state의 생성과 동시에 초기값을 useState 함수에 인자로 넣어주면, statesetState를 배열 형태로 리턴해준다. 현재 상태값은 state에 저장되고, 상태를 변경하고 싶을 땐 setState함수를 이용한다.

const [name, setName] = useState("hyun");

물론 statesetState의 이름은 우리가 원하는 대로 정할 수 있다. 사람이라는 컴포넌트 안에 name이 있다면, 이를 변경하고 싶을 때는setState("hyun123") 과 같이 인자로 업데이트할 값을 넘겨주면 된다.

setState를 이용해 state를 변경하면, 해당 컴포넌트는 화면에 다시 업데이트(렌더링)이 된다.

예제 1. Primitive type의 state

import React, { useState } from 'react';

function App() {    
  return (
    <>
      <span> 숫자 : 1 </span>
      <button> 업데이트 </button>
    </>
  )
}

export default App;

간단한 페이지를 구성해보았다. 지금은 업데이트 버튼을 눌러도 아무런 반응이 없는 상태이다. 여기서 state를 하나 추가해보자.

import React, { useState } from 'react';

function App() {    
  console.log("렌더링🖌");
  const [num, setNum] = useState(1);
  const handleClick = () => {
    setNum(num+1);
  }
  return (
    <>
      <span> 숫자 : { num } </span>
      <button onClick = { handleClick }> 업데이트 </button>
    </>
  )
}


export default App;

num, setNumuseState로 추가하고, 버튼을 클릭할 때 num을 1씩 증가해 주는 handleClick을 정의했다.

이 업데이트 버튼을 누를 때마다 state가 변경되므로 해당 컴포넌트가 브라우저 상에 다시 그려지게(렌더링) 된다. 렌더링이 될 때마다num에는 1이 증가된 값이 들어있게 된다.

예제 2. Object type의 state

이번에는 배열 값을 가지는 state를 사용하고, 업데이트 하는 예제이다.

import { useState } from 'react';

function App() {    
  const [people, setPeople] = useState(["철수","영희"]);
  
  return (
    <>
        <input type="text" /> 
        <button> 업데이트 </button>
        { people.map((person, idx)=>{
          return <p> {person} </p>
        })}
    </>
  )
}

export default App;

people이라는 state안에 철수,영희의 배열이 들어 있고, map 반복문으로 people 배열에 속해 있는 사람들을 화면에 출력하고 있다. 이제 input에 이름을 입력하고 업데이트 버튼을 누르면 people 안에 새로운 사람을 추가하는 코드를 작성해 볼 것이다.

import { useState } from 'react';

function App() {    
  const [people, setPeople] = useState(["철수","영희"]);
  const [input, setInput] = useState("");

  const handleInput = (e) => {
    setInput(e.target.value);
  }
  console.log(input)  
  return (
    <>
        <input type="text" value={input} onChange={ handleInput } /> 
        <button> 업데이트 </button>
        { people.map((person, idx)=>{
          return <p> {person} </p>
        })}
    </>
  )
}

export default App;

input이라는 state를 추가하여 input에 들어오는 값을 저장하게 했다. input 창에 값이 변화할 때마다 handleInput이 호출되어 input에 업데이트 된다. 콘솔창으로 확인해 보면 아래와 같이 될 것이다.

그 다음으로는 버튼을 눌렀을 때 input에 들어있는 값을 people에 추가해주면 된다.

import { useState } from 'react';

function App() {    
  const [people, setPeople] = useState(["철수","영희"]);
  const [input, setInput] = useState("");

  const handleInput = (e) => {
    setInput(e.target.value);
  }

  const handleUpdate = (e) => {
    setPeople((prev) => {
      return ([input, ...prev]);
    })
  }
  return (
    <>
        <input type="text" value={input} onChange={ handleInput } /> 
        <button onClick={ handleUpdate }> 업데이트 </button>
        { people.map((person, idx)=>{
          return <p> {person} </p>
        })}
    </>
  )
}

export default App;

handleUpdate라는 함수를 만들고, 버튼이 클릭될 때 호출되도록 onClick에 걸어두었다. 예를 들어 민수를 추가한다고 할 때, setPeople(["철수","영희","민수"]) 이렇게 들어가야 한다. 즉 setPeople([기존 state, 추가할 state])가 되어야 한다.

 const handleUpdate = (e) => {
    setPeople((prev) => {
      return ([input, ...prev]);
    })
  }

...prev에는 철수,영희가 있고 input에는 민수가 들어갈 것이다. 결과를 확인해보면 잘 동작한다.

예제 3. 성능 최적화

앞에서 state가 업데이트 될 때마다 컴포넌트가 다시 렌더링된다고 했다. 만약에 우리가 people의 초기값을 가져올 때, 외부 API를 이용해서 값을 불러온다거나, 복잡한 가공을 거쳐야 한다거나 해서 무겁고 오래 걸리는 작업을 거친다고 가정해 보자. 그러면 계속해서 렌더링을 해야 하기 때문에 성능이 저하될 것이다. 그 상황을 한번 예제로 만들어보고 해결해 보자.

import { useState } from 'react';

const getPeople = () => {
  console.log("People 정보를 가져오는 중 💦"); // 무겁고 오래 걸리는 작업
  return ["철수", "영희"];
}

function App() {    
  const [people, setPeople] = useState(getPeople());
  const [input, setInput] = useState("");

  const handleInput = (e) => {
    setInput(e.target.value);
  }

  const handleUpdate = (e) => {
    setPeople((prev) => {
      return ([input, ...prev]);
    })
  }
  return (
    <>
        <input type="text" value={input} onChange={ handleInput } /> 
        <button onClick={ handleUpdate }> 업데이트 </button>
        { people.map((person, idx)=>{
          return <p> {person} </p>
        })}
    </>
  )
}

export default App;

예제 2와 똑같은 코드에, getPeople이라는 함수를 만들고 people의 초기값으로 세팅해줬다.

input창에 글자를 쓸 때마다, input state가 업데이트된다. state가 업데이트 되면 컴포넌트가 다시 렌더링 되고, 컴포넌트가 렌더링 되면 컴포넌트 내부 값들이 다시 초기화되므로 people 또한 다시 초기화가 된다. 그 과정에서 무겁고 오래걸리는 작업이 계속해서 수행되는 것을 확인할 수 있다.

우리가 원하는 것은 맨 처음에 렌더링이 될 때만 getPeople이 호출되는 것이다. 그렇게 하려면 setPeople의 초기값을 넣어주는 인자에 바로 getPeople을 넣어주는 것이 아니라, 콜백함수를 넣어 주는 것이다.

import { useState } from 'react';

const getPeople = () => {
  console.log("People 정보를 가져오는 중 💦");
  return ["철수", "영희"];
}

function App() {    
  // 초기값 인자에 콜백함수 넣어주기
  const [people, setPeople] = useState(()=>{
    return getPeople();
  });
  
  const [input, setInput] = useState("");

  const handleInput = (e) => {
    setInput(e.target.value);
  }

  const handleUpdate = (e) => {
    setPeople((prev) => {
      return ([input, ...prev]);
    })
  }
  return (
    <>
        <input type="text" value={input} onChange={ handleInput } /> 
        <button onClick={ handleUpdate }> 업데이트 </button>
        { people.map((person, idx)=>{
          return <p> {person} </p>
        })}
    </>
  )
}

export default App;

input이나 people state가 변경되어도 getPeople이 호출되지 않는 것을 확인할 수 있다.

초기값을 가져올 때 무거운 작업을 해야 한다면 바로 안에 값을 넣는 것이 아니라, 우리가 원하는 값을 리턴해주는 콜백 함수를 넣어주자. 그러면 맨 처음에 렌더링될 때만 불려지게 된다.

정리

  • const [state, setState] = useState("초기값");과 같은 형태로 사용한다.
  • setState()를 사용해서 state를 변경할 때마다 컴포넌트는 다시 렌더링이 된다.
  • state를 변경할 때 변경할 값이 기존 값과 연관이 되어 있다면 (기존 값에 추가하거나 삭제) 다음과 같이 사용한다.
	setState((prev) => {
      	// 추가하거나 삭제하는 작업
    	return newState;
    })
  • useState()를 사용해서 초기화를 할 때, 무거운 일을 해야 한다면 인자로 콜백함수를 넣어주면 첫 렌더링 때만 실행되게 할 수 있다.
profile
프론트엔드를 공부하고 있습니다.

0개의 댓글