4장 - Rendering an array

54306 단어 ReactReact

Array Rendering

이번에는 리액트에서 배열을 렌더링하는 방법을 알아보겠다. 아래와 같이 배열이 있고 그 안에는 객체 형태로 되어있는 users 변수를 작성해보자.

const users = [
  {
    id: 1,
    username: '홍길동',
    email: '홍길동@gmail.com'
  },
  {
    id: 2,
    username: '이순신',
    email: '이순신@example.com'
  },
  {
    id: 3,
    username: '강감찬',
    email: '강감찬@example.com'
  }
];

만약에 이 내용을 컴포넌트로 렌더링 해야 한다면 어떻게 해야 할까? 🤔

Method 1. Rendering one by one

일단, 가장 쉽게 생각해볼 수 있으면서 가장 기본적인 방법으로는 그냥 그대로 코드를 작성하는 것 이다.

import React from 'react';

function UserList() {
  const users = [
    {
      id: 1,
      username: '홍길동',
      email: '홍길동@gmail.com',
    },
    {
      id: 2,
      username: '이순신',
      email: '이순신@example.com',
    },
    {
      id: 3,
      username: '강감찬',
      email: '강감찬@example.com',
    },
  ];

  return (
    <div>
      <div>
        <b>{users[0].username}</b> <span>{users[0].email}</span>
      </div>
      <div>
        <b>{users[1].username}</b> <span>{users[1].email}</span>
      </div>
      <div>
        <b>{users[2].username}</b> <span>{users[2].email}</span>
      </div>
    </div>
  );
}

export default UserList;

처음 몇개는 괜찮을지 모르겠지만 배열 원소 개수가 많아지면 코드 양도 길어지고 비효율적이다.

Method 2. Using the function map

가장 일반적이고 많이 쓰는 방법은 바로 자바스크립트 배열 내장함수 중에 map을 이용하는 방법이다. map은 배열 안의 각 원소를 변환 할 때 사용되며, 이 과정에서 새로운 배열이 만들어 반환해준다.

  return (
    <div>
      {users.map((user) => (
        <User user={user} />
      ))}
    </div>
  );
  • 나머지 부분은 다 같고 UserList가 리턴하는 부분을 users.map 함수를 이용하는 걸로 바꿔주었다. users.map하면 user에는 개별 원소가 들어가고, <User user={user} />를 반환해준다.
  • 하지만 이렇게 끝내면 키 경고가 나온다. "Key" props 는 각 원소들마다 고유한 값을 줌으로써 리렌더링 성능을 최적화할 수 있도록 도와주는 역할을 한다.
  return (
    <div>
      {users.map((user) => (
        <User user={user} key={user.id} />
      ))}
    </div>
  );

key를 넘겨줄때는 고유한 값, 다시말해 중복되지 않는 값을 넣어줘야 한다. 여기서는 user.id가 중복되지 않을거 같으므로 user.id를 넣어주었다. 정 넣어줄 값이 없다면 권장하지는 않지만 index를 넣는 방법도 있긴하다.

What is a key?

예를 들어 [a,b,c,d]가 있는데 c 앞에 z를 넣어준다고 가정해보자. 그렇게 되면 일단 c가 z로 바꾸고, d가 c로 바뀌고, d가 새로 생기게 된다. 마찬가지로 a를 없애본다고 가정했을 때도 a를 제거하고 그 자리는 b로 바뀌고, b는 c로 바뀌고... 이는 엄청 비효율적이다.

하지만 키를 지정해주면 이야기가 달라진다. 키가 있다면 이제 어떤 값을 렌더링하는지 알 수 있게 된다. 이러면 c 앞에 z를 넣어준다고 다시 가정했을때 그냥 b와 c 사이에 z를 추가하면 된다는 것이다.


useRef Hook

배열에 항목을 추가/제거/수정을 하는 실습을 하기 전에 먼저 useRef에 대해 알아야 할 필요가 있다. useRef는 쓰임새가 크게 2가지가 있다.

1. useRef로 특정 DOM 선택하기

개념

JavaScript 를 사용 할 때에는, 우리가 특정 DOM 을 선택해야 하는 상황에 getElementById(), querySelector() 같은 DOM Selector 함수를 사용해서 DOM 을 선택했었다.

리액트를 사용하는 프로젝트에서도 가끔씩 DOM 을 직접 선택해야 하는 상황이 발생 할 때도 있다. 예를 들어 아래와 같은 상황에서는 어쩔 수 없이 DOM을 직접 선택해야 한다.

  • 특정 엘리먼트의 크기를 가져와야 하는 경우
  • 스크롤바 위치를 가져오거나 설정해야 하는 경우
  • 포커스를 설정해줘야 하는 경우
  • Video.js, JWPlayer 같은 HTML5 Video 관련 라이브러리, 또는 D3, chart.js 같은 그래프 관련 라이브러리 등의 외부 라이브러리를 사용해야 하는 경우

그럴 땐, 리액트에서 ref 라는 것을 사용한다. 그리고 함수형 컴포넌트에서 ref를 사용할 때에는 useRef 라는 Hook 함수를 사용해야 한다.

실습

저번시간에 만든 InputForm에서 초기화 버튼을 누르면 포커스가 초기화 버튼에 그대로 남아있는데 useRef를 사용해서 초기화 버튼을 클릭했을때 이름 input에 포커스가 잡히도록 구현해보도록 하자.

import React, { useState } from "react";

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

  const onChange = (e) => {
    const { name, value } = e.target;

    setInputs({
      ...inputs,
      [name]: value,
    });
  };
  const onReset = () => {
    setInputs({
      name: "",
      email: "",
    });
    nameInput.current.focus();};

  return (
    <div>
      <input name="name" placeholder="이름" onChange={onChange} value={name} />
      <input
        name="email"
        placeholder="이메일"
        onChange={onChange}
        value={email}
        ref={nameInput}/>
      <button onClick={onReset}>초기화</button>
      <hr />
      <div>
        <b>: </b>
        {name} ({email})
      </div>
    </div>
  );
}

export default InputForm;
  • useRef()를 사용하여 Ref 객체를 만들고 이 객체를 우리가 선택하고 싶은 DOM 에 ref 값으로 설정해주어야 한다.
  • 그러면, Ref 객체의 .current 값은 우리가 원하는 DOM 을 가리키게 된다.
  • 따라서 nameInput.current는 email input을 가리키고 있고 거기서 focus() DOM API를 호출하므로써 포커스가 되도록 해주었다.

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

개념

useRef Hook 은 DOM 을 선택하는 용도 외에도, 다른 용도가 한가지 더 있는데 바로 컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리하는 것이다.

useRef는 일반적인 자바스크립트 객체이다. 즉 heap 영역에 저장되기 때문에 어플리캐이션이 종료되거나 가비지 컬렉팅 될때까지 참조할때 마다 같은 메모리 주소를 가지게 되고 값이 바뀌어도 리렌더링 되지 않는다.

하지만 함수 컴포넌트 내부에 변수를 그냥 선언한다면 리렌더링 될때마다 값이 초기화 되는 문제가 발생한다. 혹은 useState()와 같은 상태관리 Hook을 사용한다면 상태값이 변경될때마다 불필요한 리렌더링이 발생할 것이다.

useRef로 만든 변수를 사용하여 다음과 같은 값을 관리할 수 있다.

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

실습

우리는 useRef를 사용해서 앞으로 배열에 새 항목을 추가할때 새 항목에서 사용할 고유 id를 관리하는 용도로 써보도록 하겠다.

// App.js
import React, { useRef } from "react";
import UserList from "./UserList";

function App() {
  const users = [
    {
      id: 1,
      username: "홍길동",
      email: "홍길동@gmail.com",
    },
    {
      id: 2,
      username: "이순신",
      email: "이순신@example.com",
    },
    {
      id: 3,
      username: "강감찬",
      email: "강감찬@example.com",
    },
  ];

  const nextId = useRef(4);// 배열에 항목을 추가하는 함수
  const onCreate = () => {
    // (... 아직 구현 안함 ...)
    nextId.current += 1;};

  return <UserList users={users} />;
}

export default App;

useRef()를 사용 할 때 파라미터를 넣어주면, 이 값이 .current 값의 기본값이 된다. 그리고 이 값을 수정할 때는 .current 값을 수정하면 되고 조회할 때에는 .current 를 조회하면 된다. 이렇게 useRef 를 사용하므로써 얻는 장점은 리렌더링 되더라도 상태를 유지할 수 있으며 상태 값을 수정하더라도 불필요하게 리렌더링 하지 않는다는 점이다.

// UserList.js
import React from "react";

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

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

export default UserList;

Practice 1. Add Array Item

배열에 항목을 추가하기 위해서는 username과 email을 입력받는 input 2개와 button 1개가 필요하다. 우리는 이것을 CreateUser.js라는 컴포넌트로 분리해서 만들어보겠다.

CreateUser.js

import React from 'react';

function CreateUser({ username, email, onChange, onCreate }) {
  return (
    <div>
      <input
        name="username"
        placeholder="계정명"
        onChange={onChange}
        value={username}
      />
      <input
        name="email"
        placeholder="이메일"
        onChange={onChange}
        value={email}
      />
      <button onClick={onCreate}>등록</button>
    </div>
  );
}

export default CreateUser;

상태관리를 CreateUser에서 하지 않고 부모 컴포넌트인 App에서 하게 하고 input의 값 및 이벤트로 등록할 함수들을 props로 넘겨받아서 사용하도록 했다.

App.js

import React, { useRef, useState } from "react";
import UserList from "./UserList";
import CreateUser from "./CreateUser";

function App() {
  const [inputs, setInputs] = useState({ // input 상태관리
    username: "",
    email: "",
  });
  const { username, email } = inputs;

  const [users, setUsers] = useState([ // 배열 상태관리
    {
      id: 1,
      username: "홍길동",
      email: "홍길동@gmail.com",
    },
    {
      id: 2,
      username: "이순신",
      email: "이순신@example.com",
    },
    {
      id: 3,
      username: "강감찬",
      email: "강감찬@example.com",
    },
  ]);

  const nextId = useRef(4);
  
  // input 상태 변화
  const onChange = (e) => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value,
    });
  };

  // 배열에 항목 추가하는 함수
  const onCreate = () => {
    // 배열에 항목은 어떻게 추가할까? 🤔
    // 한번 생각해서 작성해보길 바란다.
    
    setInputs({
      username: "",
      email: "",
    });
    nextId.current += 1;
  };
  
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} />
    </>
  );
}

export default App;

이제 배열에 항목을 추가해볼 차례이다.

배열에 변화를 줄떼에는 객체와 마찬가지로 불변성을 지켜줘야 한다. 그렇기 때문에 배열의 push, splice, sort 등의 함수를 사용하면 안된다. 불변성을 지키면서 배열에 새 항목을 추가하는 방법은 2가지가 있다.

Method 1. spread

const onCreate = () => {
  const user = {
    id: nextId.current,
    username,
    email,
  };
  
  // `[...users]`는 `[user[0], user[1], user[2], ...]`와 동일한 의미
  setUsers([...users, user]);
  setInputs({
    username: "",
    email: "",
  });
  nextId.current += 1;
};

Method 2. concat function

const onCreate = () => {
  const user = {
    id: nextId.current,
    username,
    email,
  };
  // concat과 비슷한 함수로는 push가 있고 일반적으로 push가 더 많이 쓰이지만
  // 불변성을 유지하기 위해서 여기서는 concat을 사용했다.
  setUsers(users.concat(user));
  setInputs({
    username: "",
    email: "",
  });
  nextId.current += 1;
};

Practice 2. Remove Array Item

이번에는 배열에서 특정 user를 삭세해보는 구현을 해보자.

UserList.js

import React from "react";

function User({ user, onRemove }) {
  return (
    <div>
      <b>{user.username}</b> <span>({user.email})</span>
      <button onClick={() => onRemove(user.id)}>삭제</button>
    </div>
  );
}

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

export default UserList;

App.js

// 배열에 항목 삭제하는 함수
const onRemove = (id) => {
  // user.id가 넘겨받은 id와 일치하지 않는 원소만 추출해서 새로운 배열을 만들어서 넘겨줌
  // 즉 user.id가 id인 것은 제거됨
  setUsers(users.filter((user) => user.id !== id));
};

삭제하는 방법도 간단하다. App.js에서 onRemove 함수를 만들어서 UserList에 넘겨주고 삭제버튼을 클릭 시에 onRemove에다가 해당 user.id를 넣고 호출하면 된다.


Practice 3. Modify Array Item

마지막으로 User 컴포넌트에서 계정명을 클릭했을 때 색상이 초록색으로 바뀌고 다시 누르면 검은색으로 바뀌도록 구현해보도록 하자.

우선 그러기 위해서는 App 컴포넌트의 users 배열 안의 객체에다가 active라는 속성을 추가해주자. active가 true이면 초록색, false이면 검은색으로 할 것이다. 그 다음에 다음과 같이 style을 줘서 active값에 따라 color값이 바뀌도록 삼함연산자를 사용했다.

<b style={{
    cursor: "pointer",
    color: user.active ? "green" : "black",
}}
   onClick={() => onToggle(user.id)}
>
{user.username}
</b>

이제 App.js에서 onToggle이라는 함수를 만들어서 user을 클릭하면 active 값을 반전시키도록 구현할 것이다.

// 배열 active 수정하는 함수
const onToggle = (id) => {
  setUsers(
    users.map((user) =>
      user.id === id ? { ...user, active: !user.active } : user
    )
  );
};

수정할때는 map 함수를 사용해서 배열의 불변성을 유지하면서 배열을 업데이트할 수 있다.

✍ 정리해보면 배열 생성 시에는 spread 연산 혹은 concat을 사용, 배열 삭제시에는 filter을 사용, 배열 수정 시에는 map을 사용

좋은 웹페이지 즐겨찾기