State Hooks (1) useState

Doozuu·2025년 3월 20일
0

React

목록 보기
25/30
post-thumbnail

useState

useState는 컴포넌트에 state variable을 더할 수 있도록 도와주는 훅이다.

기본 형태

const [state, setState] = useState(initialState)

Reference

useState(initialState)

state의 네임 컨벤션은 [something, setSomething] 형태이다.

Parameters

  • initialState : 초기값, 어떤 타입의 값이든 상관없지만 함수에 대해선 특수하게 동작한다.
    initialState로 넘기는 함수는 initializer function으로 다루어진다.
    이때 initializer function은 인자를 받지 않는 순수 함수여야 한다.

Returns
1. current state : 첫 렌더링 후에는 설정해 둔 initialState가 들어감.
2. set function : state를 업데이트 하는 함수. 리렌더링을 발생시킴.

주의사항

  • useState는 hook이므로 컴포넌트 최상단에서만 호출할 수 있다. (반복문, 조건문 내부에서 호출하면 안 됨. 필요한 경우 새로운 컴포넌트를 만들어서 옮기기)
  • Strict Mode에서는 React가 initializer function을 두 번 호출해서 예상치 못한 부작용을 찾는 데 도움을 준다. 개발중에만 발생하고 프로덕션에는 발생하지 않는다.

set functions

set 함수는 state를 업데이트하고 리렌더링을 발생시킨다.
이때 set 함수로

  1. 업데이트 할 값을 직접적으로 넘기거나
  2. 이전 state를 바탕으로 계산하는 함수를 넘길 수 있다.
  setName('Taylor');
  setAge(a => a + 1);

Parameters
어떤 값이든 상관없지만 함수는 특수하게 동작한다.
함수를 전달하면 updater function으로 처리된다.
이때 함수는 순수 함수여야 하고, 대기 중인 state를 유일한 인자로 가져야 하고, 다음 state를 반환해야한다.

Returns
값을 반환하지 않는다.

주의사항

  • set function은 다음 렌더링을 위해 state를 업데이트 한다. set 함수를 호출한 후 state를 읽으면 호출 이전 값을 얻게 된다. (아직 리렌더링이 발생하지 않았기 때문)
function handleClick() {
  setName('Robin');
  console.log(name); // Still "Taylor"!
}
  • 새로 전달한 값이 현재 state와 동일하다면 React는 최적화를 위해 리렌더링을 건너뛴다.
  • React는 state 업데이트를 일괄적으로 처리한다. 이벤트 핸들러와 set 함수가 호출된 후에 화면이 업데이트 된다.(하나의 이벤트 중에 여러 번 렌더링 되는 것을 막기 위함.) 화면을 더 일찍 업데이트해야 하는 경우에는 flushSync를 사용할 수 있음.
  • Strict Mode에서는 React가 updater function을 두 번 호출해서 예상치 못한 부작용을 찾는 데 도움을 준다. 개발중에만 발생하고 프로덕션에는 발생하지 않는다.

Usage

기본적인 예시 ) counter, input field, checkbox, form

import { useState } from 'react';

export default function MyCheckbox() {
  const [liked, setLiked] = useState(true);

  function handleChange(e) {
    setLiked(e.target.checked);
  }

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={liked}
          onChange={handleChange}
        />
        I liked this
      </label>
      <p>You {liked ? 'liked' : 'did not like'} this.</p>
    </>
  );
}

Updating state based on the previous state

아래의 경우 age는 45가 되는 대신 43이 된다.
(set function이 state를 바로 업데이트하지 않기 때문.)

function handleClick() {
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
}

이럴 때 updater function을 넘기면 의도대로 만들 수 있다.

function handleClick() {
  setAge(a => a + 1); // setAge(42 => 43)
  setAge(a => a + 1); // setAge(43 => 44)
  setAge(a => a + 1); // setAge(44 => 45)
}

updater function은 pending state를 받아서 next state를 계산한다.
React는 updater function을 queue에 넣고 다음 렌더링 전에 계산한다.
1. a => a + 1 will receive 42 as the pending state and return 43 as the next state.
2. a => a + 1 will receive 43 as the pending state and return 44 as the next state.
3. a => a + 1 will receive 44 as the pending state and return 45 as the next state.
더 이상 queue에 업데이트할 사항이 없으므로 45를 current state로 저장하게 됨.

pending state argument는 state의 첫 글자로 짓는게 컨벤션이다.
ex) age -> a
혹은 prevAge와 같이 짓는 것도 괜찮다.
React는 updater 함수가 순수 함수인지 검증하기 위해 개발 모드에서 updater 함수를 두 번 호출할 수 있다.

updater 함수를 쓰는게 항상 좋은가?
이전 값을 바탕으로 계산해야 할 때 항상 updater function을 쓰는 것은 불필요하다.
하지만 같은 이벤트 내에서 업데이트를 여러번 해야 할 때는 updater function이 유용할 수 있다. state 변수 자체에 접근하는 것이 어려운 상황에도 유용하다.(ex. 리렌더링 최적화할 때)
만약 state가 다른 state의 이전 값으로부터 계산된다면 하나의 객체로 결합해서 reducer를 사용하는 것이 좋다.

Updating objects and arrays in state

React에서는 state가 읽기 전용으로 간주되기 때문에 기존 객체를 변경(mutate)하기 보다는 교체(replace)해야 한다.

// x
form.firstName = 'Taylor';

// o
setForm({
  ...form,
  firstName: 'Taylor'
});

https://react.dev/learn/updating-objects-in-state

중첩된 객체인 경우 업데이트 하는 예제

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}

배열 업데이트 하는 예제

import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';

let nextId = 3; 
const initialTodos = [
  { id: 0, title: 'Buy milk', done: true },
  { id: 1, title: 'Eat tacos', done: false },
  { id: 2, title: 'Brew tea', done: false },
];

export default function TaskApp() {
  const [todos, setTodos] = useState(initialTodos);

  function handleAddTodo(title) {
    setTodos([
      ...todos,
      {
        id: nextId++,
        title: title,
        done: false
      }
    ]);
  }

  function handleChangeTodo(nextTodo) {
    setTodos(todos.map(t => {
      if (t.id === nextTodo.id) {
        return nextTodo;
      } else {
        return t;
      }
    }));
  }

  function handleDeleteTodo(todoId) {
    setTodos(
      todos.filter(t => t.id !== todoId)
    );
  }

  return (
    <>
      <AddTodo
        onAddTodo={handleAddTodo}
      />
      <TaskList
        todos={todos}
        onChangeTodo={handleChangeTodo}
        onDeleteTodo={handleDeleteTodo}
      />
    </>
  );
}

배열과 객체를 변경할 때 mutation을 피하는 것이 번거롭다면 Immer과 같은 라이브러리를 사용해 반복적인 코드를 줄일 수 있다.
Immer를 사용하면 마치 객체를 mutation하는 것처럼 간결한 코드를 사용하면서 내부적으로는 불변성을 유지할 수 있다.

import { useState } from 'react';
import { useImmer } from 'use-immer';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [list, updateList] = useImmer(initialList);

  function handleToggle(artworkId, nextSeen) {
    updateList(draft => {
      const artwork = draft.find(a =>
        a.id === artworkId
      );
      artwork.seen = nextSeen;
    });
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={list}
        onToggle={handleToggle} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

Avoiding recreating the initial state

React는 초기값을 한 번만 저장하고 이후 렌더링에서는 무시한다.
아래에서 createInitialTodos()의 결과값은 초기 렌더링에서만 쓰이지만 매번 렌더링할 때마다 해당 함수를 실행하게 된다.
만약 해당 함수가 큰 배열을 만들거나 계산을 많이 할 경우 낭비일 수 있다.

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos());
  // ...

이를 해결하기 위해서는 함수 자체를 넘기면 된다.
이렇게 되면 React는 초기화할 때만 해당 함수를 실행한다. (개발중에서는 순수 함수인지 확인하기 위해 두 번 실행할 수도 있음.)
https://react.dev/learn/keeping-components-pure

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos);
  // ...

Resetting state with a key

리스트를 렌더링할 때 자주 사용하는 key를 다른 용도로 쓸 수 있다.
컴포넌트에 다른 key 값을 전달하면 컴포넌트 상태를 초기화할 수 있다.

아래에서 Reset 버튼을 누르면 version이 변경되고(1,2,3,,) 변경된 값을 Form에 key로 전달하여 Form을 리셋할 수 있다.

import { useState } from 'react';

export default function App() {
  const [version, setVersion] = useState(0);

  function handleReset() {
    setVersion(version + 1);
  }

  return (
    <>
      <button onClick={handleReset}>Reset</button>
      <Form key={version} />
    </>
  );
}

function Form() {
  const [name, setName] = useState('Taylor');

  return (
    <>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <p>Hello, {name}.</p>
    </>
  );
}

Storing information from previous renders

보통 state는 이벤트 핸들러에서 업데이트한다. 그러나 간혹 렌더링에 따라 state를 조정하고 싶을 수 있다. (ex. props가 변경될 때 state를 변경)

대부분의 경우는 필요하지 않다.

  • 필요한 값이 현재 props나 다른 state에서 계산될 수 있다면, 중복된 state를 아예 제거해라. 만약 너무 자주 재계산되는 것이 걱정된다면, useMemo Hook을 쓸 수 있다.
  • 전체 컴포넌트 트리의 state를 초기화하려면, 컴포넌트에 다른 key 값을 전달해라.
  • 가능하면 모든 관련 state를 이벤트 핸들러에서 업데이트해라.
    이들 중 어느 것도 적용되지 않는 드문 경우에는, 렌더링 중에 set 함수를 호출하여 지금까지 렌더링된 값을 기반으로 상태를 업데이트하는 패턴을 사용할 수 있다.

props로 받은 값이 증가되었는지 감소되었는지 알고 싶은 경우 이전 값을 저장하는 state를 만들어 추적할 수 있다.
렌더링 중에 set 함수를 호출하면, 컴포넌트가 무한 루프에 빠지거나 예기치 않은 동작을 일으킬 수 있다. 이를 방지하기 위해서는 조건문을 사용해서 안전하게 state를 업데이트 해야 한다.
아래 패턴은 가능하면 피하는 것이 좋다. 이해하기 어렵고 예기치 않은 문제를 일으킬 수 있기 때문이다.
일반적으로는 이벤트 핸들러나 useEffect 안에서 이루어진다. (useEffect는 렌더링 후 실행되므로 무한 루프에 빠지지 않을 수 있다.)
렌더링 중에 상태를 업데이트하는 것은 가능하면 피하는 것이 좋다.

import { useState } from 'react';

export default function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  
  // count가 변경될 때만 setPrevCount를 호출함
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'increasing' : 'decreasing');
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </>
  );
}

Troubleshooting

1. state를 업데이트 했는데 로그에 이전 값이 찍히는 경우

-> set function가 실행됐을 때 state가 바로 변경되는게 아니다.
-> state가 snapshot처럼 동작하기 때문. (state는 특정 시점의 스냅샷이기 때문에 state를 업데이트해도 실행 중인 코드에는 변화가 즉시 반영되지 않음.)
-> state는 비동기적으로 업데이트 된다. state 변경 요청을 React에 보내고 그 요청은 다음 렌더링에서 처리된다.

function handleClick() {
  console.log(count);  // 0

  setCount(count + 1); // Request a re-render with 1
  console.log(count);  // Still 0!

  setTimeout(() => {
    console.log(count); // Also 0!
  }, 5000);
}

따라서 업데이트 된 값을 쓰고 싶다면 값을 set function에 전달하기 전에 변수에 저장해두는게 좋다.

const nextCount = count + 1;
setCount(nextCount);

console.log(count);     // 0
console.log(nextCount); // 1

2. state를 업데이트했는데 화면에 업데이트되지 않은 경우

React는 이전 state와 다음 state가 같은 경우 업데이트를 무시한다. (Object.is로 비교하여 결정됨)
주로 객체나 배열을 변경할 때 위와 같은 문제가 발생한다.

obj.x = 10;  // 🚩 Wrong: mutating existing object
setObj(obj); // 🚩 Doesn't do anything

이를 해결하기 위해서는 객체와 배열을 mutate하지 말고 replace해야 한다.

// ✅ Correct: creating a new object
setObj({
  ...obj,
  x: 10
});

3. “Too many re-renders” 에러를 겪는 경우

해당 에러는 렌더링 중에 state를 변경하려고 했을 때 발생한다.
(렌더링 -> state 변경 -> 렌더링 -> state 변경...의 무한 루프에 빠지게 됨)

주로 이벤트 핸들러를 잘못 지정했을 때 발생한다.

// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>

// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>

// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>

4. initializer 함수나 updater 함수가 두 번씩 실행되는 경우

Strict Mode에서는 React가 state 업데이트 함수를 중복 실행한다. (개발중에만 실행되는 동작)
React가 일부러 두 번 호출해서 컴포넌트를 순수하게 유지할 수 있도록 돕는 것.
initializer 함수나 updater 함수가 순수하다면 로직에 영향이 없어야 한다.

function TodoList() {
  // This component function will run twice for every render.

  const [todos, setTodos] = useState(() => {
    // This initializer function will run twice during initialization.
    return createTodos();
  });

  function handleClick() {
    setTodos(prevTodos => {
      // This updater function will run twice for every click.
      return [...prevTodos, createTodo()];
    });
  }
  // ...

아래와 같이 순수하지 않은 함수를 사용한 경우 실수를 미리 잡아낼 수 있다.
(React가 updater 함수를 두 번 실행하기 때문에 todo가 두 번 더해진 것을 보고 실수를 알아챌 수 있다.)

setTodos(prevTodos => {
  // 🚩 Mistake: mutating state
  prevTodos.push(createTodo());
});

아래처럼 mutate를 replace로 바꾸어서 문제를 해결할 수 있다.
(updater 함수를 두 번 호출해도 동작에 영향 없음. -> todo가 정상적으로 한 번만 추가됨.)

setTodos(prevTodos => {
  // ✅ Correct: replacing with new state
  return [...prevTodos, createTodo()];
});

component, initializer, updater function만 순수 함수면 된다.
(이벤트 핸들러는 순수할 필요가 없으므로 React가 두 번 호출하지 않음)

5. state로 함수를 전달하려는데 함수 호출이 될 때

함수를 전달하면 React는 해당 함수를 initializer 함수(아래에서는 someFunction)로 추정하거나 updater 함수(아래에서는 someOtherFunction)로 추정한다.

const [fn, setFn] = useState(someFunction);

function handleClick() {
  setFn(someOtherFunction);
}

따라서 함수 자체가 전달되지 않고, 함수를 실행해서 값을 저장하려고 한다.
(함수 자체를 저장하려면 () => someFunction 형태로 넘겨야 함.)

const [fn, setFn] = useState(() => someFunction);

function handleClick() {
  setFn(() => someOtherFunction);
}

컴포넌트를 순수하게 유지한다는 것은 무엇인가?

몇몇 자바스크립트 함수는 순수하다.
순수한 함수는 calculation만 수행하고 이외의 다른 것은 하지 않는다.
컴포넌트에 순수 함수만 작성함으로써 예상치 못한 동작이나 버그를 피할 수 있다.

What purity is and how it helps you avoid bugs

순수 함수란 무엇인가?

  • 함수와 관련되지 않은 다른 값에 영향을 주지 않는다.
  • 같은 값을 넣었을 때 항상 같은 값을 출력한다.

순수 함수는 수학 공식과 유사하다.
y = 2x
-> x에 2를 넣으면 항상 y는 4가 된다.
-> x에 3를 넣으면 항상 y는 6이 된다.

이를 javascript 함수로 만들면 아래와 같을 것이다.
이와 같은 함수를 순수 함수라고 한다.

function double(number) {
  return 2 * number;
}

React는 이 개념을 중심으로 설계되었다.
React는 모든 컴포넌트가 순수 함수라고 가정한다. 즉, React component는 같은 입력값이 주어졌을 때 항상 동일한 JSX를 반환해야 한다.

재료가 바뀌지 않는 한 항상 같은 결과를 얻게 된다는 점에서 컴포넌트를 레시피라고 생각할 수도 있다. 이때 얻게 되는 dish를 JSX라고 생각할 수 있다.

Side Effects: (un)intended consequences

React의 렌더링 과정은 항상 순수해야 한다.
컴포넌트는 렌더링 이전에 존재하던 어떤 값도 변경해선 안 된다.

아래 예제 코드는 컴포넌트 외부에서 선언된 변수를 읽고 쓰고 있기때문에 순수하지 않다.
-> 이 컴포넌트를 실행할 때마다 다른 JSX를 생성하게 된다. (예측 가능성이 떨어진다.)

let guest = 0;

function Cup() {
  // Bad: changing a preexisting variable!
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup /> // 2
      <Cup /> // 4
      <Cup /> // 6
    </>
  );
}

guest 값을 prop으로 넘겨주어 해결할 수 있다.

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} /> // 1
      <Cup guest={2} /> // 2
      <Cup guest={3} /> // 3
    </>
  );
}

일반적으로 컴포넌트가 특정 순서대로 렌더링되리라고 기대해선 안 된다.
각 컴포넌트는 독립적으로 실행되고, 그래야 하기 때문이다.

Detecting impure calculations with StrictMode
Strict Mode를 사용하면 개발 중에 각 컴포넌트의 함수를 두 번 호출하여 impure한 컴포넌트를 찾아내는데 도움을 얻을 수 있다.

  • 위의 impure한 함수 예제에서 2,4,6이 출력된 이유는 해당 함수가 impure하기 때문이다. (strict mode에 의해 두 번 실행되었을 때 오류가 발생함. pure하다면 실행 횟수와 상관없이 같은 값을 출력해야 함. 따라서 수정된 pure function은 두 번 호출해도 제대로 동작함.)
  • same input, same output 원칙을 지키는지 확인하는 것.
  • Strict Mode는 프로덕션 환경에서는 아무 영향을 미치지 않으므로 앱 속도에 영향을 미치지 않는다. Strict Mode를 사용하려면 <React.StrictMode>로 루트 컴포넌트를 감싸면 된다. 일부 프레임워크는 기본적으로 이를 활성화한다.

Local mutation: Your component’s little secret

위처럼 컴포넌트가 렌더링 중에 기존 변수를 변경하는 것을 mutation이라고 부른다.

순수 함수는 함수 범위를 벗어나거나 호출 이전에 생성된 객체나 변수를 변경하지 않는다.
하지만 아래와 같이 렌더링 중에 생성한 변수나 객체를 변경하는 것은 문제가 되지 않는다.

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

만약 cups가 TeaGathering 외부에서 선언되었다면 큰 문제가 되겠지만 TeaGathering를 내부에서 생성한 것은 문제가 되지 않는다.
TeaGathering 외부의 코드는 해당 사실을 알지 못하기 때문이다.
이를 local mutation이라 부른다.

Where you can cause side effects

함수형 프로그래밍은 순수성을 매우 중요시하지만 변경을 해야만 하는 시점도 있다.
이러한 변경들(화면 업데이트, 애니메이션 시작, 데이터 변경 등)을 side effects라고 부른다.

  • side effects는 렌더링 중에는 발생하지 않는다.
  • React에서는 side effects가 주로 이벤트 핸들러 안에 존재한다.
  • 이벤트 핸들러는 React가 특정 작업을 수행할 때 실행되는 함수다. (ex. 버튼 클릭 시 호출되는 함수)
  • 이벤트 핸들러는 컴포넌트 내부에서 정의되지만, 렌더링 중에는 실행되지 않아서 이벤트 핸들러는 순수 함수일 필요가 없다.
  • 적합한 이벤트 핸들러를 찾을 수 없는 경우 useEffect를 사용해 side effects를 담을 수 있다. 이러면 React가 렌더링을 한 후에 side effects가 허용되는 시점에 코드가 실행된다. (하지만 최후의 수단으로 쓰는 것이 좋음. 가능하다면 렌더링만으로 논리를 표현하려고 노력할 것)

Why does React care about purity?
순수 함수를 작성하는 것의 이점은 다음과 같다.
1. 컴포넌트를 다른 환경에서 실행할 수 있다.
2. 성능을 개선할 수 있다.
입력이 변경되지 않는 컴포넌트는 렌더링을 건너뛸 수 있다. 순수 함수는 항상 동일한 결고를 반환하므로 캐시할 수 있다는 점에서 안전함.
3. 깊은 계층의 컴포넌트 트리를 렌더링하는 중에 데이터 변경이 발생한다면 React는 렌더링을 완료하지 않고 렌더링을 재시작할 수 있다. purity를 지키면 언제든 계산을 멈춰도 안전하도록 해줌.
React의 모든 새로운 기능은 purity의 이점을 활용한다. 데이터 fetching부터 성능 향상까지 컴포넌트를 pure하게 유지하는 것이 React 패러다임의 힘을 발휘하게 해준다.


예제 문제) 비순수 함수를 순수 함수로 고쳐보기

Challenge 1 of 3: Fix a broken clock

export default function Clock({ time }) {
  let hours = time.getHours();
  if (hours >= 0 && hours <= 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return (
    <h1 id="time">
      {time.toLocaleTimeString()}
    </h1>
  );
}

Hint : Rendering is a calculation, it shouldn’t try to “do” things.

내가 고친 답

export default function Clock({ time }) {
  let hours = time.getHours();
  const type = hours >= 0 && hours <= 6 ? 'night' : 'day'  

  return (
    <h1 className={type}>
      {time.toLocaleTimeString()}
    </h1>
  );
}

정답
className을 계산하고 render output에 포함하여 해결할 수 있다.
이 경우 DOM을 수정하는 side effect는 불필요하다.

export default function Clock({ time }) {
  let hours = time.getHours();
  let className;
  if (hours >= 0 && hours <= 6) {
    className = 'night';
  } else {
    className = 'day';
  }
  return (
    <h1 className={className}>
      {time.toLocaleTimeString()}
    </h1>
  );
}

Challenge 2 of 3: Fix a broken profile

두 개의 서로 다른 Profile 컴포넌트를 렌더링 했을 때, collapse 버튼을 클릭하고 다시 expand하면 프로필이 같은 인물로 바뀌는 버그가 발생함. 버그 원인을 찾고 고쳐보자.

import Panel from './Panel.js';
import { getImageUrl } from './utils.js';

let currentPerson;

export default function Profile({ person }) {
  currentPerson = person;
  return (
    <Panel>
      <Header />
      <Avatar />
    </Panel>
  )
}

function Header() {
  return <h1>{currentPerson.name}</h1>;
}

function Avatar() {
  return (
    <img
      className="avatar"
      src={getImageUrl(currentPerson)}
      alt={currentPerson.name}
      width={50}
      height={50}
    />
  );
}

내가 고친 답

  • 추정 원인 : currentPerson을 컴포넌트 외부에서 선언한 것이 문제. 처음에는 다른 인물이 할당되어 다르게 렌더링 되어 있지만 collapse하고 expand하면서 리렌더링이 발생. currentPerson이 가장 마지막에 할당한 인물로 되어 있어 두 프로필 인물이 동일하게 렌더링 됨.
  • 해결 방법 : currentPerson을 따로 선언하지 않고 props로 받은 인자를 바로 전달
import Panel from './Panel.js';
import { getImageUrl } from './utils.js';

export default function Profile({ person }) {
  return (
    <Panel>
      <Header name={person.name}/>
      <Avatar currentPerson={person}/>
    </Panel>
  )
}

function Header({name}) {
  return <h1>{name}</h1>;
}

function Avatar({currentPerson}) {
  return (
    <img
      className="avatar"
      src={getImageUrl(currentPerson)}
      alt={currentPerson.name}
      width={50}
      height={50}
    />
  );
}

혹은 추상화 단계를 낮춘다면 아래처럼도 가능할 것 같다.

import Panel from './Panel.js';
import { getImageUrl } from './utils.js';

export default function Profile({ person }) {
  const name = person.name;
  
  return (
    <Panel>
      <h1>{name}</h1>
      <img
        className="avatar"
        src={getImageUrl(person)}
        alt={name}
        width={50}
        height={50}
     />
    </Panel>
  )
}

정답
currentPerson이라는 컴포넌트 외부 값을 쓰는 것이 문제.
currentPerson 변수를 없애고 profile과 header에 props로 값을 전달.
React는 컴포넌트 함수의 실행 순서를 보장하지 않으므로 컴포넌트 간에 변수를 사용해 상태를 공유할 수 없음.
컴포넌트 간의 상태나 데이터 전달은 반드시 props를 통해서 이루어져야 함.

import Panel from './Panel.js';
import { getImageUrl } from './utils.js';

export default function Profile({ person }) {
  return (
    <Panel>
      <Header person={person} />
      <Avatar person={person} />
    </Panel>
  )
}

function Header({ person }) {
  return <h1>{person.name}</h1>;
}

function Avatar({ person }) {
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={50}
      height={50}
    />
  );
}

Challenge 3 of 3: Fix a broken story tray

Create Story가 여러 번 출력되는 버그 해결하기

export default function StoryTray({ stories }) {
  stories.push({
    id: 'create',
    label: 'Create Story'
  });

  return (
    <ul>
      {stories.map(story => (
        <li key={story.id}>
          {story.label}
        </li>
      ))}
    </ul>
  );
}

내가 고친 답

  • 추정 원인 : stories에 직접 값을 push하면서 렌더링이 무한 루프에 빠지게 됨.(렌더링 -> push -> 렌더링 -> push..)
    => 정확히는 무한 루프에 빠지는게 아니라 strict mode에서 두 번씩 실행할 때 impure 함수이므로 계속 추가됨.
  • 헤결 방법 : 기존 값에 객체를 합친 새로운 배열을 반환함.
export default function StoryTray({ stories }) {
  const createStory = { id: 'create', label: 'Create Story' };
  const newStories = [...stories, createStory];

  return (
    <ul>
      {newStories.map(story => (
        <li key={story.id}>
          {story.label}
        </li>
      ))}
    </ul>
  );
}

정답
위에 시계가 업데이트될 때마다 create story가 두 번씩 추가됨.
-> 렌더링 될 때 mutation이 발생한다는 힌트가 됨.
StoryTray가 렌더링되기 이전에 만들어진 값을 변경하는 것이 문제의 원인임.
가장 간단한 해결책은 array를 아예 건들지 않고 “Create Story”를 따로 렌더링하는 것.

export default function StoryTray({ stories }) {
  return (
    <ul>
      {stories.map(story => (
        <li key={story.id}>
          {story.label}
        </li>
      ))}
      <li>Create Story</li>
    </ul>
  );
}

자료

profile
모든게 새롭고 재밌는 프론트엔드 새싹

0개의 댓글