개발 공부/React

가장 쉬운 리액트 7~8강

5묘 2022. 12. 15. 19:53
가장 쉬운 웹개발 with Boaz

7.Lists and Keys

 

7.Lists and Keys

(React 공식문서의 main concepts 번역 글입니다.) 함께보면 이해가 쏙쏙 https://youtu.be/7xZMLyfsQc8component로 list를 구성할 때key를 활용하여 각 component를 구분해야 한다. 그래야만 ReactDOM이 각 component를 비

davidhwang.netlify.app

더보기

React에서 array의 아이템 하나하나를 component로 만들어 리스트에 저장한다.

 

다수의 component를 rendering 하기(list 만들기)

elements를 item으로 갖는 array를 만들 수 있다. 이 array를 {}를 통해 JSX안에서 사용할 수 있다.

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map(number =>
  <li>{number}</li> // react element를 return 하는 component
);

<li>{number}</li>는 element를 return하는 component이다.(map이 배열을 return 하는 함수이므로 component 맞다.)
이 element들은 listItems라는 array의 item이다.
이 array는 element(<li> tag)들의 집합이므로
<ul>로 감싼 후, ReactDOM.render()에게 전달하여 rendering한다.

ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('root')
);

다수의 component를 rendering하는 코드가 완성되었다.

List를 return하는 component

component들로 구성된 list를 return하는 component를 만들어보자.

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map(number =>    
    <li>{number}</li>  
  );  
  return (
    <ul>{listItems}</ul>  
  )
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,  
  document.getElementById('root')
);

위 코드를 실행하면 경고가 뜬다!
react에서 list는 각 item마다 key값이 필요하기 때문이다.

key 는 string값을 가지는 특수한 props이다. react에서 list는 각 item마다 key 값을 주어야 한다.

위 list의 각 item(<li>{number}</li>)에 key값을 주어
경고 를 해결하자.

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>      
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

Keys

key는 list의 각 item이 수정되거나 item이 추가, 삭제될 때
react가 그것을 빠르게 알아차릴 수 있도록 도와준다.

map을 실행할 때 item에게 반드시 key값이 주어져야 한다.

그래야 item, 즉 react element가 stable한 identity를 가질 수 있다.

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li key={number.toString()}>    
    {number}
  </li>
);

가장 좋은 key 값은 다른 element들과 다른 unique한 string 값이다.
대부분은 data의 id값 key값으로 준다.

const todoItems = todos.map((todo) =>
  <li key={todo.id}>    
    {todo.text}
  </li>
);

만약 stable한(변하지 않는) key값을 주기가 정말 어렵다면 index를 key값으로 써라.

const todoItems = todos.map((todo, index) =>
  // Only do this if items have no stable IDs  
  <li key={index}>    
    {todo.text}
  </li>
);

item의 index를 key값으로 주는 것은 추천하지 않는다.
item의 순서가 바뀔 수 있기 때문이다.(index값이 stable하지 않음)
그 결과, react performance가 떨어질 수 있다.
또한 component의 state에 관한 issue가 발생 할 수도 있다.
(index를 key값으로 주면 안좋은 이유에 대해 추가설명한 링크
https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318)

index를 key로 주게 되면 이렇게 add Item해서 index 달라졌을때 넣은 value값이 달라질 수 있다.

(++ 그래서 id 값이나 nanoid를 쓰는 방법이 추천된다.)

만약 key값을 주지 않으면, react는 우선 index를 key로 사용 한다.

 key가 react performance와 연관있는 걸까?
자세히 알아보려면 아래 링크를 확인하자
(https://reactjs.org/docs/reconciliation.html#recursing-on-children)
react는 <li> element 요소들 흝으며 같은 것(즉 key 같은 것)이면 묶어서 처리하고, 다른 것이 나오면 그때 mutation 일으키는데 만약 이 자료 예시처럼 add 되는 것이 맨 앞에 나온다던가, key가 없게 되면 똑같은 걸 match 시키지 못한 채 같은 것임에도 mutation(복사본) 만들어야 해서 비효율적이다.

Extracting Components with Keys

keys값은 array의 item을 map으로 바꾸는 과정에서 주어져야 한다.
extract한 component가 element를 return 할 때
key값을 주는 것은 의미가 없다.

key값을 잘 못 준 경우(ReactDOM.render는 생략)

function ListItem(props) {
  const value = props.value;
  return (
    // Wrong! There is no need to specify the key here:    
    <li key={value.toString()}>      
      {value}
    </li>
  );
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // Wrong! The key should have been specified here:    
    <ListItem value={number} />  
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

key값을 바르게 준 경우(ReactDOM.render는 생략)

function ListItem(props) {
  // Correct! There is no need to specify the key here:  
  return <li>{props.value}</li>;
  }

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // Correct! Key should be specified inside the array.    
    <ListItem key={number.toString()}              
    value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

즉, key 값은 map 함수 사용 시에 줘야만 한다.

key값은 unique해야 한다(siblings과 비교해서)

siblings란 array안에 함께 있는 다른 component들을 말한다.
이 component들과는 구별되기 위해 unique한 key값을 가져야한다.
하지만 global하게 모든 component들과 다른, unique한 key값을 가질 필요는 없다.

즉, 다른 component들의 집합안에 있다면 같은 key값을 사용해도 괜찮다. 아래 코드를 보며 이해해보자.

function Blog(props) {
  const sidebar = (
    <ul>
      {props.posts.map((post) =>
     ** <li key={post.id}>          
          {post.title}
        </li>
      )}
    </ul>
  );
  
  const content = props.posts.map((post) =>  //sidebar과 content가 다른 array이므로 key값 같아도 상관 없다.
 ** <div key={post.id}>      
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}

const posts = [
  {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
  {id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
  <Blog posts={posts} />,
  document.getElementById('root')
);

첫번째 하이라이팅된 부분과 두번째 하이라이팅된 부분이
같은 key값을 각 list의 item(<div>와 <li>)에게 주고 있다.
<div> return하는 component와 <li> return하는 component
다른 component들의 모음이기 때문에
같은 key값(post.id)을 할당해도 error가 발생하지 않는다.

key값은 React가 알아차리는데 도움을 줄 뿐,
component에게 props로 전달되지 않는다.
따라서, key값을 component 안에서 활용하고 싶다면
새로운 다른 props로 값을 할당해주어야 한다.

const content = posts.map((post) =>
  <Post
    key={post.id}    
 ** id={post.id}   
    title={post.title} 
  />
);

위에서 처럼, post.id값을 content안에서 props로 활용하고 싶다면
id라는 props를 통해 그 값을 새롭게 다시 할당해 주어야 한다.

JSX안에 map을 통째로 넣기

아래 코드에서는 listItems 변수에 component의 lists를 할당하고
JSX안에서 그 변수를 {listItems}로 넣은 것을 볼 수 있다.

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>    
  <ListItem key={number.toString()}
            value={number} />
  );
  return (
    <ul>
      {listItems}    
    </ul>
  );
}

JSX는 어떠한 javascript expression도 넣을 수 있으므로
{}안에 map 전체를 넣을 수도 있다.

function NumberList(props) {
  const numbers = props.numbers;
  return (
    <ul>
      {numbers.map((number) =>        
      <ListItem key={number.toString()}                  
                value={number} />      
      )}    
    </ul>
  );
}

가끔 map 전체를 JSX안에 넣으면 코드가 더 깔끔해질 수도 있다.
가독성이 더 좋은 방향으로 코드를 짜면 된다.

만약 map 안에서 여러 브라켓으로 nested 되어있다면
component를 extract하는 것이 더 가독성이 좋을 것이다.

 

8. Forms

 

8.Forms

(React 공식문서의 main concepts 번역 글입니다.) 함께보면 이해가 쏙쏙 https://youtu.be/9XoL9WDhYeY React에서,form element 다른 DOM element와는 조금 다르게 다뤄진다.form은 내부에 state를 이미 가지고 있기 때문

davidhwang.netlify.app

더보기

React에서, form element는 다른 DOM element와는 조금 다르게 다뤄진다.
form은 내부에 state를 이미 가지고 있기 때문이다.
아래 예시는 name 값을 받는 form이다.

<form>
  <label>
    Name:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>

위 form은 사용자가 form 내용을 submit 할 때
기본적으로 새로운 페이지로 이동하는 동작을 한다.

만약에 React에서도 똑같이 동작하길 원한다면
그대로 React에서 똑같이 사용하면 된다.(정상 작동 함)

그러나 form을 submit하는 function이 필요할 때가 있다.
또한 user가 form에 입력하는 data에 접근해야할 때도 있다.
그럴때 React에서는 controlled component을 사용한다. => form에서 이미 가진 state와 React의 state를 합침.

controlled component

form element는 <input>, <textarea>, <select>등이 있다.
 form element들은 각자 state를 갖고 있으며
user input값에 따라 그 state 값을 자유롭게 업데이트 할 수 있다.
React에서는 이러한 state값은 component의 property이며
오직 setState()를 사용해서만 업데이트 할 수 있다.

위에 form의 state와 React의 state를 하나로 합치자.
하나로 합친 그 state를 관리하는 component를 만들자.
그 component가 바로 controlled component이다.

controlled component
<input>처럼 state를 가진 tag를 render하며
<input>의 state 또한 관리한다.

그 결과 form element들(ex)<input>)이 입력 받은 data를
관리할 수 있게 된다.

class NameForm extends React.Component {
  constructor(props) {
    super(props);
 ** this.state = {value: ''};  
  }

  handleChange = (event) => {
    this.setState({value: event.target.value});
  }

  handleSubmit = (event) => {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return(
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
       ** <input type="text" value={this.state.value} onChange={this.handleChange} />        
        </label>
        <input type="submit" value="Submit" />
      </form>
    )
  }
}

위 NameForm component은 controlled component이다.

먼저, 첫번째 하이라이팅 부분에서 state를 확인할 수 있다.
이 state는 마지막 하이라이팅 부분, <input> value값에 할당된다.
<input>(form element)이 입력받은 value값으로
NameForm component가 자신의 state값을 수정한다.
즉 state를 하나로 합쳤다.

<input>이 동작하는 순서는 아래와 같다.

  1. 사용자가 <input>에 값을 입력한다.
  2. onChange에 할당된 handleChange가 실행된다.
  3. setState에 의해 state값이 바뀐다.
    (사용자에게서 입력받은 값으로 바뀜)
  4. <input>의 value값에 바뀐 state값이 할당된다.
  5. render()가 다시 실행되어 <input>이 rendering된다.

controlled component는 모든 form element state의 변화를
handle하는 handler function을 가진다.
그 결과, 사용자에게 입력을 받거나 입력값을 수정하는 것이 간단해진다.

만약 위 <input>이 입력받은 값을 모두 대문자로 바꾸고 싶을 때는

handleChange(event) {
  this.setState({value: event.target.value.toUpperCase()});
}

위와 같이 value값을 수정해주면 간단하게 바꿀 수 있다.

textarea tag

HTML에서 <textarea>(form element)는 text가 children이다.

<textarea>
  Hello there, this is some text in a text area
</textarea>

React에서 <textarea>는 value를 props로 가진다.
이때 이 <textarea>를 포함하는 form의 형태는
<input>을 포함하는 form의 형태와 매우 비슷하다.

class EssayForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
   ** value: 'Please write an essay about your favorite DOM element.' // Text area의 children 값   
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('An essay was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Essay:
       ** <textarea value={this.state.value} onChange={this.handleChange} />        
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

첫번째 하이라이팅된 부분에서 state값이 초기화 되었으므로
이 textarea는 render될 때 이미 값(state값  'Please write an essay about your favorite DOM element.' )이 쓰여져있다.

++ 왜 textarea가 포함된 태그에서만 이벤트핸들러에 대해 this 바인딩이 일어나는 것인가?🤔(유튜브 댓글로 질문 남김)

select tag

HTML에서 select tag(form element)는 drop-down list를 만든다.

<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>  <option value="mango">Mango</option>
</select>

selected 특성 때문에 하이라이팅된 coconut이 초기값이다.

React에서는 초기값을 지정하는 방법으로 selected 특성 대신
select tag의 value라는 특성 직접 초기값을 할당한다.
그 결과 controlled component는 state를 업데이트하기가 더 쉽다.

class FlavorForm extends React.Component {
  constructor(props) {
    super(props);
 ** this.state = {value: 'coconut'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {    
 ** this.setState({value: event.target.value});  
  }
  handleSubmit(event) {
    alert('Your favorite flavor is: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Pick your favorite flavor:
       ** <select value={this.state.value} onChange={this.handleChange}>            
            <option value="grapefruit">Grapefruit</option>
            <option value="lime">Lime</option>
            <option value="coconut">Coconut</option>
            <option value="mango">Mango</option>
          </select>
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

전반적으로 이러한 방식을 사용하면 다음 form element들
<input type="text">, <textarea>, <select>가 모두
비슷하게 동작한다.

즉 value특성의 값을 앞에 form elements에게 위 방식으로 준다면
controlled component가 간단하게 value를 업데이트 할 수 있다.

다중 선택: multiple 속성을 true로 하면 다중 선택이 가능하다. value특성에게는 array를 값으로 줄 수 있다.

<select multiple={true} value={['B', 'C']}>

file input tag

HTML에서 <input type="file">를 활용한다면
local 저장소에서 하나 혹은 여러개의 파일을 선택하고
선택한 파일을 서버에 저장하거나
File API를 통해 조작할 수 있다.

<input type="file" />

이 tag의 value값은 읽기만 가능하기 때문에
react에서는 uncontrolled component로 분류된다.

여러개 input tag 들을 동시에 활용하기

여러개 <input>들을 동시에 활용해야 할 때에는
 <input>들에게 name property를 준다.
이 name property의 값, 즉 event.target.name값에 따라
handle function이 어떤 동작을 할지 결정하게 한다.

class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isGoing: true,
      numberOfGuests: 2
    };

    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
 ** const name = target.name;
    
    this.setState({
   ** [name]: value    
    });
  }

  render() {
    return (
      <form>
        <label>
          Is going:
          <input
         ** name="isGoing"            
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange} />
        </label>
        <br />
        <label>
          Number of guests:
          <input
         ** name="numberOfGuests"            
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange} />
        </label>
      </form>
    );
  }
}

동작 원리는 아래와 같다.

  1. <input>의 type은 2가지(checkbox와 value 입력)
  2. handleInputChange는 name이라는 변수를 선언
  3. name에는 event.target.name 이 할당됨
  4. setState()에서 state의 key값은 [name]
  5. <input>의 type이 변하면 <input>의 name값도 변함
  6. name값이 변하면 state의 key값도 변함
  7. setState가 수정하는 state의 값도 변함
  8. setState가 name값에 따라 다른 state 값을 수정함

또한 state값을 수정하기 위해 우리는 state 객체의 key값을
[name]로 표현했다.(computed property name 활용)

this.setState({
  [name]: value
});

위 코드는 아래와 같은 결과를 가져온다.

var partialState = {};
partialState[name] = value;
this.setState(partialState);

setState()가 partialState를 현재 state에 merge하므로
state값이 바뀔 때만 setState()가 실행된다.

Controlled input null value

controlled component에 value props 값을 지정해주면
원하지 않는 경우 <input> value가 바뀌는 걸 막을 수 있다.

만약 value props값을 지정해줬음에도 <input>value가 바뀐다면
실수로 props값을 undefined이나 null로 지정해줬을 수 있다.

아래 예시를 통해 이러한 경우를 확인할 수 있다.

ReactDOM.render(<input value="hi" />, mountNode); //hi가 떠있다가 

setTimeout(function() {
  ReactDOM.render(<input value={null} />, mountNode); // 1초후 사라지고 입력 가능 상태로 바뀜
}, 1000);

// 입력 받고 싶음 null, 수정하고 싶지 않으면 null, undefined 넣으면 안됨.

controlled components의 대안?

모든 <input>에 대해 state를 관리하고
handle function을 만드는 것이 비효율적으로 느껴질 수 있다.
uncontrolled component를 만날 때 form의 한계(state를 다룰 수 없음😥)를 느낄수도 있다. 

uncontrolled component의 이런 한계점에 Formik(https://jaredpalmer.com/formik/)은 해결책이 될수 있다.


https://blog.songc.io/react/react-form-control/ form을 다룰때 react-hook-form이나 formik을 쓰면 state를 관리하고 this.state.value로 변경하지 않더라도 form data 제어 가능함. 이 자료는 react-hook-form과 formik 차이 정리한 글.

'개발 공부 > React' 카테고리의 다른 글

가장 쉬운 리액트 11강  (0) 2022.12.16
가장 쉬운 리액트 10강  (0) 2022.12.16
가장 쉬운 리액트 9강  (0) 2022.12.16
[TIL] virtual DOM  (0) 2022.12.15
가장 쉬운 리액트 1~4강  (0) 2022.12.13