본문 바로가기

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

[2024 React.js 스터디] 류상우 #5주차

반응형
10. useRef 로 특정 DOM 선택하기

React에서 특정 DOM을 선택하기 위해서 필요한 게 바로 ref이다. ref를 사용할 때에는 useRef라는 Hook함수를 사용한다. 어떤 요소를 클릭했는지 표시하는 컴포넌트를 만들어보자.

//Colors.js
import React, { useRef } from 'react';

function Colors() {
    const color = useRef();
    const onClick = e => color.current.innerText = 'You clicked ' + e.target.style.backgroundColor;

    return (
        <>
            <div id='boxes'>
                <div className='box' style={{ backgroundColor: 'red' }} onClick={onClick}></div>
                <div className='box' style={{ backgroundColor: 'blue' }} onClick={onClick}></div>
                <div className='box' style={{ backgroundColor: 'green' }} onClick={onClick}></div>
            </div>
            <p ref={color} style={{ margin:10, fontSize:30 }}></p>
        </>
    );
}

export default Colors

state를 사용하는 게 더 좋겠지만 이렇게 ref를 사용할 수도 있다.

각각 빨간색/파란색/초록색인 3개의 정사각형 중 하나를 클릭하면 p태그의 텍스트가 클릭한 정사각형의 색을 눌렀다는 글로 바뀌게 된다.

 

이를 위해서 우선 ref객체를 만든다.

const color = useRef();

 

이후 선택하고 싶은 DOM의 ref값으로 설정해주어야 한다.

<p ref={color} style={{ margin:10, fontSize:30 }}></p>

 

그러면 ref객체 Color의 .current 값은 해당 DOM 을 가르키게 된다. 이후 onClick 이벤트가 발생했을 때 해당 DOM의 innerText를 바꿔준다.

const onClick = e => {
  color.current.innerText = 'You clicked ' + e.target.style.backgroundColor;
};

11. 배열 렌더링하기

React에서 배열을 렌더링 하는 방법을 알아보자

이러한 배열이 있다고 할 때 어떻게 렌더링 해야할까?

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'
  }
];

 

우선 그냥 그대로 코드를 작성하는 방법이 있다.

function UserList() {
  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[1].email})</span>
      </div>
    </div>
  );
}

 

다음으로는 반복되는 부분에서 컴포넌트를 재사용하는 방법이다.

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

function UserList() {
  return (
    <div>
      <User user={users[0]} />
      <User user={users[1]} />
      <User user={users[2]} />
    </div>
  );
}

첫 번째 방법보다는 낫지만 인덱스를 하나씩 조회해가며 렌더링 하는 방법은 동적인 배열을 렌더링할 수 없다.

 

동적인 배열을 렌더링하기 위해서는 JS의 내장함수 map()을 사용한다. map() 은 배열 안의 요소를 변화시켜 새로운 배열을 만들어주며 파라미터로 변화를 주는 함수를 전달해주면 된다.

map() 을 사용해 일반 데이터 배열을 리액트 엘리먼트로 이루어진 배열로 변환해주자.

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

이렇게 해도 렌더링은 가능하지만 React에서 배열을 렌더링 하려면 key라는 props를 설정해야 한다. 여기서 key는 각 요소들이 가지는 고유 값으로 설정해야 한다. 배열 users 같은 경우에는 id가 고유 값이다.

<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를 설정하는 이유: 키가 없다면 리렌더링을 할 때 필요한 부분만 수정하는 게 아니라 달라진 부분부터 모든 요소를 수정하기 때문에 비효율적이다.


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

앞에서 배웠던 useRef는 특정 DOM을 선택할 때 말고 또다른 사용법이 존재한다. 바로 컴포넌트 내에서 조회 및 수정할 수 있는 변수를 관리하는 것이다. 또한 useRef로 관리하는 변수의 값이 바뀐다고해서 리렌더링되지 않고, 리렌더링하지 않아도 변경된 값에 접근할 수 있다. 이 변수로 다음과 같은 값을 관리할 수 있다.

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

각각 useState, useRef, let 으로 선언한 변수는 어떤 차이점을 가질까?

다음은 3가지 방식으로 만든 변수를 가지는 Counter 예제이다.

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

function Counter() {
  const [a, setA] = useState(0);
  const b = useRef(0);
  let c = 0;

  const onIncrease = () => {
    setA(a + 1);
    b.current++
    c++
    console.log(`${a}/${b.current}/${c}`)
  }

  return (
      <button onClick={onIncrease}>Click!</button>
  );
}

export default Counter;

다음은 버튼을 5번 클릭했을 때의 콘솔 창이다.

분명 a, b, c 모두 기본값은 0이지만 출력은 다르다.

  • useState: useState로 관리하는 a는 리렌더링이 되기 전의 값 즉, setA(a + 1)이 실행되기 전의 값이므로 0부터 출력된다.
  • useRef: useRef로 관리하는 b는 설정 후 바로 조회할 수 있으므로 1부터 출력된다.
  • let: let으로 선언한 c는 렌더링 할 때마다 값이 초기화 되므로 항상 1만 출력된다.

13. 배열에 항목 추가하기

배열에 새로운 항목을 추가하는 방법을 알아보자. 배열에 변화를 줄 때에는 객체 마찬가지로 불변성을 지켜야 한다. 그렇기 때문에 push, slice, sort 등의 함수를 사용해서는 안된다. 만약 사용해야 한다면 배열을 한 번 복사한 뒤 사용해야 한다.

 

불변성을 지키면서 배열에 새 행목을 추가하는 방법에는 spread연산자를 사용하는 방법이 있다.

다음은 spread연산자를 사용한 예제이다. username과 email을 입력한 뒤 등록 버튼을 누르면 배열에 추가된다.

//App.js
import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([]);

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

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} />
    </>
  );
}

export default App;
//User.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;

//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;

이 컴포넌트에서는 상태관리를 부모 컴포넌트인 App에서 하고, input의 값 및 이벤트로 등록할 함수들을 props로 넘겨받아 사용한다. 또한 배열 users를 useState를 사용해 상태로서 관리한다.

 

이벤트로 등록할 함수들을 App에서 선언하고 props로 넘겨받다는 점이 이전과 달라 어색하지만 8,9장에서 봤던 코드와 크게 다르지 않다.

등록 버튼을 클릭했을 때 실행되는 onCreate()를 살펴보자

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

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

users에 새로 추가할 요소인 user객체를 선언한 뒤 setUsers()를 사용해 users에 추가한다. 이 떼 spread 연산자 ... 을 사용해서 불변성을 지키며 요소를 추가한다.

 

또 다른 방법은 concat 함수를 사용하는 것이다.  concat 함수는 기존의 배열을 수정하지 않고 새로운 원소가 추가된 새로운 배열을 만들어준다.

setUsers(users.concat(user));

14. 배열에 항목 제거하기

불변성을 지키면서 배열에 항목을 제거할 때는 filter 함수를 사용한다. filter 함수는 배열에서 특정 조건이 만족하는 원소들만 추출하여 새로운 배열을 만들어준다.

 

우선 위에서 작성했던 userList.js에 onClick 이벤트가 발생했을 때 onRemove()를 실행하는 삭제 버튼을 추가하고 App.js에 onRemove()를 작성한 뒤 userList에 props로 전달해준다.

//userList.js
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>
  );
}
const onRemove = id => {
  setUsers(users.filter(user => user.id !== id));
};

onRemove()를 호출할 때 고유 값인 user.id를 파라미터로 전달한다. 그럼 filter를 통해 배열에서 user.id가 전달된 값과 다른 요소들만 남게 된다.

<button onClick={() => onRemove(user.id)}>삭제</button>
<button onClick={onRemove(user.id)}>삭제</button>

여기서 아래 코드가 아니라 위 코드를 사용하는 이유는 다음과 같다.

onClick={onRemove(user.id)}로 작성 시 userList컴포넌트가 렌더링될 때 onRemove()가 실행되어 버린다. 이러한 문제를 해결하기 위해서 onClick에 콜백 함수를 넣어주고, 해당 함수가 실행될 때 user.id를 건네주어 실행시키는 방법으로 처리를 하는 것이다.


15. 배열에 항목 수정하기

계정 명을 클릭했을 때 색상이 초록색/검정색으로 바뀌도록 해보자.

우선 userList.js에서 user.active 값에 따라 계정 명의 색이 바뀌도록 하고, onClick시 onToggle()이 실행되도록 한다.

//userList.js
function User({ user, onRemove, onToggle }) {
  return (
    <div>
      <b
        style={{
          cursor: 'pointer',
          color: user.active ? 'green' : 'black'
        }}
        onClick={() => onToggle(user.id)}
      >
        {user.username}
      </b>
      &nbsp;
      <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>
  );
}

 

이후 App.js에 onToggle()을 작성하고 userList에 props로 onToggle을 전달한다.

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

map()을 사용해 users의 요소 중 user.id 가 전달받은 id와 같은 경우에만 active 값을 반전시킨다.

이때 user.active값을 설정하지 않아 user.active의 기본 값은 undefined인데 JS에서 undefined의 boolean은 false이므로 해당 코드가 정상적으로 작동한다.

user.active 가 없을 때는 user.active: true를 추가하고 있을 때는 값을 반전시키는 것이다.


16. useEffect를 사용하여 마운트/언마운트/업데이트시 할 작업 설정하기

useEffect라는 Hook은 컴포넌트가 마운트 됐을 때 (처음 나타났을 때), 언마운트 됐을 때 (사라질 때), 그리고 업데이트 될 때 (특정 props가 바뀔 때) 특정 작업을 처리하도록 할 수 있다.

 

버튼을 클릭하면 box컴포넌트가 마운트/언마운트 되도록 코드를 작성해보자

//App.js
import React, {useState} from 'react';
import Box from './Component/Box';

function App() {
  const [isShowed, set] = useState(true)
  const onClick = () => set(!isShowed)

  return (
    <>
    <button onClick={onClick}>click!</button>
    {isShowed ? <Box Color={Color}} /> : undefined}  
    </>
  );
}

export default App;
//Box.js
import React, { useEffect } from 'react';

function Mount ({ Color, colorChange }) {
    useEffect(() => {
      console.log('컴포넌트가 화면에 나타남');
      return () => {
        console.log('컴포넌트가 화면에서 사라짐');
    };}, []);

    return (
        <div style={{ width:100, height:100, backgroundColor:'black' }} />
    );
};

export default Mount;

useEffect 를 사용 할 때에는 첫번째 파라미터에는 함수, 두번째 파라미터에는 의존값이 들어있는 배열 (deps)을 넣는다. 만약에 deps 배열을 비우게 된다면, 컴포넌트가 처음 나타날때에만 useEffect 에 등록한 함수가 호출된다.

 

그리고, useEffect 에서는 함수를 반환 할 수 있는데 이를 cleanup 함수라고 부른다. cleanup 함수는 useEffect 에 대한 뒷정리를 해준다고 이해하면 되는데, deps 가 비어있는 경우에는 컴포넌트가 사라질 때 cleanup 함수가 호출된다.

 

즉 이 경우에는 deps 배열이 비워져 있으므로 컴포넌트가 처음 호출될 때와 사라질 때만 동작한다.

 

그런데 콘솔을 확인하면 이상한 점이 보인다.

컴포넌트가 두 번씩 호출되는 것이다.

이는 React의 strict Mode 때문이다. react strict mode는 개발 모드에서만 활성화 되고, 개발자가 애플리케이션의 잠재적인 문제를 더 쉽게 확인할 수 있도록 도와준다. 이것 때문에 컴포넌트가 두 번 실행된 것인데 이를 해결하려면 index.js에서 <App.js>를 감싸고 있는 <React.StrictMode>를 제거하면 된다. 하지만 strict mode는 개발에 중요한 역할을 하므로 이러한 방법은 지양하는게 좋다.

  • 마운트 시에 주로 하는 작업
    • props 로 받은 값을 컴포넌트의 로컬 상태로 설정
    • 외부 API 요청 (REST API 등)
    • 라이브러리 사용 (D3, Video.js 등)
    • setInterval 을 통한 반복잡업 혹은 setTimeout 을 통한 작업 예약
  • 언마운트 시에 주로 하는 작업
    • setInterval, setTimeout 을 사용하여 등록한 작업들 clear하기 (clearInterval, clearTimeout)
    • 라이브러리 인스턴스 제거

위에서는 deps를 비워뒀지만 deps에 특정 값을 넣으면 동작이 달라진다.

 

deps 에 특정 값을 넣게 된다면, 컴포넌트가 처음 마운트 될 때에도 호출이 되고, 지정한 값이 바뀔 때에도 호출이 된다. 그리고, deps 안에 특정 값이 있다면 언마운트시에도 호출이되고, 값이 바뀌기 직전에도 호출이 된다.

 

App.js와 Box.js 를 다음과 같이 작성해보자.

//App.js
function App() {
  const [isShowed, set] = useState(true)
  const [Color, setColor] = useState(true)
  const onClick = () => set(!isShowed)
  const colorChange = () => setColor(!Color)

  return (
    <>
    <button onClick={onClick}>click!</button>
    {isShowed ? <Box Color={Color} colorChange={colorChange} /> : undefined}  
    </>
  );
}
//Box.js
function Mount ({ Color, colorChange }) {
    useEffect(() => {
        console.log('Color 값이 설정됨');
        console.log(Color);
        return () => {
          console.log('Color 가 바뀌기 전..');
          console.log(Color);
    };}, [Color]);

    return (
        <>
        <div style={{ width:100, height:100, backgroundColor: Color ? 'red' : 'black' }} onClick={colorChange} />
        </>
    );
};

useEffect 안에서 사용하는 상태나, props 가 있다면, useEffect  deps 에 넣어야 한다.

만약 useEffect 안에서 사용하는 상태나 props 를 deps 에 넣지 않으면 useEffect 에 등록한 함수가 실행 될 때 최신 props / 상태를 가르키지 않는다.


deps 파라미터를 생략하면 컴포넌트가 리렌더링 될 때마다 호출이 된다.

 

Box.js를 다음과 같이 작성해보자

function Mount ({ Color, colorChange }) {
    useEffect(() => {
        console.log(Color);
    });

    return (
        <>
        <div style={{ width:100, height:100, backgroundColor: 'black' }} onClick={colorChange} />
        </>
    );
};

컴포넌트가 렌더링 될 때마다 함수가 호출된다.

반응형