React_Day.2

조현기·4일 전

React_Day.2

useState로 값 바꾸기, 인라인 스타일, 입력창 다루기, 조건부 렌더링, 리스트 렌더링(map/key), 가상 DOM, props와 children.

Day.1에서 useState로 초기값을 만들어 화면에 출력하는 데까지 했다. 오늘은 값을 실제로 바꾸고, 입력창과 연결하고, 조건이나 배열에 따라 화면을 다르게 그리는 것, 그리고 컴포넌트끼리 값을 전달하는 props를 배웠다.


1. 상태 바꾸기: setState

1-1. 숫자 올리기

import React, { useState } from 'react'

export const App = () => {

  const [num, setNum] = useState(0)    // 0이 num에 대입된다

  const onClickButton = () => {
    setNum(num + 1)      // 상태 변경 함수: 새 값으로 바꾸고 다시 렌더링한다
  }

  return (
    <div>
      <button onClick={onClickButton}>클릭미</button>
      <h2>👍{num}</h2>
    </div>
  )
}

버튼을 누를 때마다 0 → 1 → 2 …로 올라간다. 흐름은 이렇다.

  1. 클릭하면 onClickButton이 실행되어 setNum(num + 1)을 호출한다.
  2. setNum이 값을 바꾸고 컴포넌트를 다시 실행(리렌더링)한다.
  3. 다시 실행된 컴포넌트에서 num은 새 값이고, <h2>가 새 값으로 그려진다.

값을 바꿀 때는 num = num + 1처럼 변수를 직접 바꾸지 않고 반드시 setNum을 통해 바꾼다. 그래야 React가 "값이 바뀌었으니 다시 그려야 한다"는 것을 알 수 있다.

1-2. 참/거짓 토글

const [opinion, setOpinion] = useState(false)

const onOpinion = () => {
  setOpinion(!opinion)       // true ↔ false 뒤집기
}

<button onClick={onOpinion}>찬성/반대</button>
<h2>{opinion ? '찬성' : '반대'}</h2>      {/* 삼항 연산자 */}

!opinion으로 값을 뒤집는다. 처음에는 false라서 반대가 보이고, 누를 때마다 찬성/반대가 번갈아 나온다.

1-3. 문자열 두 개를 바꾸는 방법

문장 두 개를 각각 버튼으로 바꾸는 연습도 했다.

const [hi, setHi] = useState('안녕')

const onClickButton1 = () => {
  if (hi == '안녕') {
    setHi('사실 안녕하지 않음.')
  } else if (hi == '사실 안녕하지 않음.') {
    setHi('안녕')
  }
}

이 방식은 바꿀 글자를 코드에 직접 비교해서 적어야 해서 문구가 바뀌면 두 곳을 모두 고쳐야 한다. 두 가지 상태를 번갈아 보이는 문제라면 위의 true/false 토글에 글자만 삼항 연산자로 붙이는 편이 간단하다.

const [isHi, setIsHi] = useState(true)

<button onClick={() => setIsHi(!isHi)}>전환</button>
<h1>{isHi ? '안녕' : '사실 안녕하지 않음.'}</h1>

한 번 더 정리한 연습에서는 버튼마다 정해진 값을 바로 넣는 방식으로 바꿨다.

const [msg, setMsg] = useState('')          // 빈 문자열로 초기화

const Clickin = () => setMsg('안녕')
const Clickout = () => setMsg('bye')

<button onClick={Clickin}>들어온다</button>
<button onClick={Clickout}>나간다</button>
<h1>{msg}</h1>

화살표 함수는 본문이 한 줄이면 { }와 return을 생략할 수 있다. (const show = (n) => n)

1-4. 같은 값으로 여러 번 바꾸면

setNum(num + 1)
setNum(num + 1)
setNum(num + 1)      // 한 번 클릭에 3이 아니라 1만 올라간다

확인해 보니 이렇게 쓰고 한 번 클릭하면 3이 아니라 1만 올라갔다. 한 번의 이벤트 안에서는 num이 그 순간의 값으로 고정되어 있어서 세 번 모두 0 + 1을 넣는 셈이기 때문이다. 이전 값을 기준으로 이어서 바꾸려면 함수 형태로 쓴다.

setNum(n => n + 1)
setNum(n => n + 1)
setNum(n => n + 1)   // 한 번 클릭에 +3

setNum에 함수를 넘기면 React가 "최신 값"을 그 함수의 인자(n)로 넣어 준다.


2. 인라인 스타일

JSX에서 style에는 문자열이 아니라 객체를 넣는다.

const con1 = {
  color: 'red',
  fontSize: '2em',
  border: '3px solid red'
}

<h1 style={con1}>HI</h1>
  • CSS 속성 이름은 camelCase로 쓴다. font-size → fontSize, background-color → backgroundColor. (JS Day.5에서 style.fontSize로 고쳤던 것과 같다)
  • 값은 문자열로 쓴다. ('2em', '3px solid red')
  • 객체를 변수로 빼서 style={con1}처럼 쓰거나, 바로 넣을 때는 중괄호 두 겹을 쓴다.
<p style={{ color: 'red', backgroundColor: 'black' }}>집으로</p>

바깥 { }는 "JSX 안에서 JavaScript를 쓴다"는 표시고, 안쪽 { }는 객체 리터럴이다. 두 겹이 필요한 이유다.

상태와 스타일을 묶으면 클릭으로 색을 바꿀 수 있다.

const [color, setColor] = useState('black')

<button
  style={{ backgroundColor: color, color: 'blue' }}
  onClick={() => setColor('red')}
>
  빨강
</button>
// 클릭하면 배경이 black → red로 바뀐다. 상태 값 'red'가 color에 저장되고 다시 그려진다.

onClick={() => setColor('red')}처럼 인자를 넘겨야 할 때는 화살표 함수로 감싼다. Day.1에서 onClick={함수()}로 쓰면 렌더링 때 바로 실행된다고 했던 이유로, 괄호가 필요한 호출은 () =>로 감싸야 클릭 때 실행된다.


3. 입력창과 상태 연결

import React, { useState } from 'react'

const App = () => {

  const [name, setName] = useState('')
  const [nickname, setNickname] = useState('')

  const onChangeName = (e) => {
    setName(e.target.value)
  }
  const onChangeNickName = (e) => {
    setNickname(e.target.value)
  }

  return (
    <div>
      <input type='text' placeholder='이름을 입력해주세요' onChange={onChangeName} />
      <input type='text' placeholder='닉네임을 입력해주세요' onChange={onChangeNickName} />
      이름 : {name}
      닉네임 : {nickname}
    </div>
  )
}

export default App
  • 입력할 때마다 onChange 이벤트가 발생하고, 이벤트 객체 e의 e.target.value에 입력창의 현재 값이 들어 있다. JS Day.7의 이벤트 객체와 같다.
  • 그 값을 setName으로 상태에 저장하면, 상태가 바뀌어서 화면의 {name}도 입력과 함께 바뀐다.
  • 입력창이 두 개라서 상태도 두 개(name, nickname)와 핸들러도 두 개를 만들었다.

실제로 홍길동을 입력하면 이름 : 홍길동으로 바로 나오는 것을 확인했다.

입력창의 값까지 상태로 관리하기

입력창의 value를 상태로 묶으면 입력창의 값을 코드에서 제어할 수 있다. 아래 4장의 목록 추가 예제처럼 추가 후 입력창을 비울 때 쓴다.

const [input, setInput] = useState('')

<input onChange={(e) => setInput(e.target.value)} value={input} />
<button onClick={() => setInput('')}>비우기</button>

value={input}으로 연결하면 입력창이 보여 주는 값이 항상 상태와 같다. 이런 입력창을 제어 컴포넌트(controlled component)라고 부른다.


4. 조건부 렌더링

4-1. 삼항 연산자

const user = {
  name: "홍길동",
  isLogin: true,
}

<div>{user.isLogin == true ? '로그인' : '로그아웃'}</div>
// isLogin이 true면 '로그인', false면 '로그아웃'

JS Day.3의 삼항 연산자를 JSX 안의 { }에서 쓴 것이다. user.isLogin == true는 user.isLogin만 써도 같다.

4-2. &&와 ||

const name = '리액트'

<div>
  {name === '리액트' ? (<h1>리액트이다</h1>) : (<h1>리액트아니다</h1>)}

  {name === '리액트' && (<h1>리액트이다</h1>)}

  {name === '리엑트' || (<h1>리액트이다</h1>)}
</div>
연산자규칙용도
a ? b : ca가 참이면 b, 아니면 c둘 중 하나를 보여 줄 때
a && ba가 참이면 b를 반환조건이 맞을 때만 보여 줄 때
a \|\| ba가 거짓이면 b를 반환조건이 아닐 때 대신 보여 줄 때

JS Day.9의 ||/&&의 반환값 규칙이 그대로 쓰인다. 실행해 보면 세 줄 모두 리액트이다가 나온다. 세 번째 줄은 비교한 글자가 '리엑트'(오타)라서 왼쪽이 거짓이 되어 오른쪽 <h1>이 나온 것이다.

JSX에서 false, null, undefined는 화면에 아무것도 그리지 않는다. 그래서 &&로 "조건이 거짓이면 아무것도 안 보이게" 만들 수 있다.

단, 숫자 0은 그려진다. 이 점이 함정이다.

const num = 0

<p>{num && <b>있다</b>}</p>         // 화면에 0 이 찍힌다
<p>{num > 0 && <b>있다</b>}</p>     // 아무것도 안 그려진다 (false)

0은 거짓이지만 0 && ...의 결과가 0 자체이고, React는 숫자 0을 화면에 그린다. 숫자로 조건을 걸 때는 num > 0 && ...처럼 비교식으로 써야 한다.


5. 리스트 렌더링: map과 key

배열의 데이터를 화면에 여러 개로 그릴 때는 map을 쓴다. (JS Day.9의 map)

import React, { useState } from 'react'

const App = () => {
  const [post, setPost] = useState([
    { id: 1, title: '게시글 제목 A', content: '내용 A' },
    { id: 2, title: '게시글 제목 B', content: '내용 B' },
    { id: 3, title: '게시글 제목 C', content: '내용 C' }
  ])

  return (
    <div>
      <h1>게시글 목록</h1>
      <hr></hr>
      {post.map((i) => {
        return (
          <div key={i.id}>
            <h2>{i.title} : {i.content}</h2>
          </div>
        )
      })}
    </div>
  )
}

export default App
  • 배열의 객체를 하나씩 꺼내(i) 태그로 바꿔서 배열로 돌려주고, React가 그 배열을 화면에 그린다.
  • 화살표 함수는 return을 생략할 수 있어서, 한 줄로 줄이면 이렇게 쓴다.
{post.map(i => <h2 key={i.id}>{i.title} : {i.content}</h2>)}

key는 왜 필요한가

반복해서 그린 요소에는 key를 붙인다. key는 각 요소를 구별하는 고유한 값(보통 id)이다. 빼먹으면 콘솔에 경고가 뜬다.

Each child in a list should have a unique "key" prop.

React는 데이터가 바뀌면 이전 결과와 새 결과를 비교해서 바뀐 부분만 업데이트한다. 이때 key로 "이 항목이 그 항목"인지 알아본다. 그래서 key는 바뀌지 않는 고유한 값이어야 하고, 배열의 순서(index)는 항목이 추가/삭제될 때 달라지기 때문에 가급적 쓰지 않는다.

목록에 항목 추가하기

입력창에 글을 쓰고 [추가]를 누르면 목록에 항목이 늘어나는 예제다. 처음 쓴 코드는 이랬다.

const [name, setName] = useState([
  { id: 1, text: '항목 1' },
  { id: 2, text: '항목 2' },
  { id: 3, text: '항목 3' }
])

const nameList = () => name.map(name1 => <li key={name1.id}>{name1.text}</li>)

const [input, setInput] = useState('')
const [nextId, setNextId] = useState(4)
const [testList, setTestList] = useState(nameList)

const onChange = (e) => setInput(e.target.value)

const addList = () => {
  name.push({ id: nextId, text: input })      // 원본 배열을 직접 바꾼다
  setInput('')
  setNextId(nextId + 1)
  setTestList(nameList)
}

<input onChange={onChange} value={input} />
<button onClick={addList}>추가</button>
<ul>{testList}</ul>

실행해 보면 x, y를 차례로 추가했을 때 항목…, x, y로 잘 동작한다. 다만 이 코드가 되는 이유가 조금 특이해서 짚어 둔다.

  • useState(nameList)와 setTestList(nameList)처럼 함수를 넘기면 React가 그 함수를 직접 호출해서 결과를 값으로 쓴다. 그래서 testList에는 함수가 아니라 <li>들의 배열이 들어간다.
  • name.push(...)는 상태로 관리하는 배열을 직접 수정한다. 이 배열은 setName을 거치지 않았기 때문에 React는 name이 바뀐 것을 모른다. 대신 바로 아래의 setTestList(nameList)가 목록을 다시 만들면서 화면이 갱신된 것이다.

React에서 상태는 직접 수정하지 않고, 새 값을 만들어서 set 함수로 바꾸는 것이 원칙이다. 그래야 React가 변경을 알아채기 때문이다. 이 원칙대로 다시 쓰면 상태를 하나로 줄일 수 있다.

const [items, setItems] = useState([
  { id: 1, text: '항목 1' },
  { id: 2, text: '항목 2' },
  { id: 3, text: '항목 3' }
])
const [input, setInput] = useState('')
const [nextId, setNextId] = useState(4)

const add = () => {
  if (!input.trim()) return                         // 빈 입력은 무시
  setItems([...items, { id: nextId, text: input }]) // 스프레드로 새 배열을 만든다
  setInput('')
  setNextId(nextId + 1)
}

<input onChange={(e) => setInput(e.target.value)} value={input} />
<button onClick={add}>추가</button>
<ul>{items.map(n => <li key={n.id}>{n.text}</li>)}</ul>
  • [...items, 새항목]은 JS Day.8의 스프레드 구문으로, 기존 항목을 복사하고 새 항목을 뒤에 붙인 새 배열을 만든다.
  • 화면용 목록(<li> 배열)을 상태로 따로 저장하지 않고, 렌더링할 때 items.map(...)으로 바로 만든다. 상태는 데이터(items) 하나만 두는 것이 간단하다.
  • 결과는 원본 코드와 같다. 같은 순서로 x, y를 추가하면 항목…, x, y가 되고 입력창은 비워진다.

6. 가상 DOM

필기에는 React가 화면을 갱신하는 과정이 세 단계로 적혀 있다.

  1. 데이터가 바뀌면 컴포넌트가 다시 실행되어 새로운 가상 DOM이 만들어진다.
  2. React가 이전 가상 DOM과 새 가상 DOM을 비교한다.
  3. 달라진 부분만 찾아서 실제 DOM에 반영한다.

가상 DOM: 실제 화면(DOM) 대신 메모리에 만들어 두는 가벼운 복사본.

실제 DOM을 매번 통째로 다시 그리지 않고, 바뀐 부분만 고치기 때문에 효율적이다. 위의 key도 이 비교 과정에서 쓰인다.

필기에는 "render()가 다시 호출된다"고 적혀 있는데, render()는 클래스형 컴포넌트의 메소드 이름이다. 지금 쓰는 함수형 컴포넌트에서는 컴포넌트 함수 자체가 다시 호출되는 것이라고 이해하면 된다.


7. props: 부모가 자식에게 값 전달하기

컴포넌트를 나누면 부모 컴포넌트가 자식 컴포넌트에게 값을 넘겨야 할 때가 생긴다. 이때 쓰는 것이 props다. HTML 태그에 속성을 쓰듯이 컴포넌트에 값을 적는다.

// App.jsx (부모)
import React from 'react'
import Button from "./components/Button"

const App = () => {
  return (
    <>
      <Button message={"리액트"} color={"pink"} />
      <Button message={"DB"} />
      <Button message={"FAST API"} />
    </>
  )
}

export default App
// components/Button.jsx (자식)
import React from 'react'

const Button = (props) => {      // props 는 컴포넌트 함수의 매개변수로 받는다
  console.log(props)
  return (
    <div>
      <button style={{ color: props.color }}>{props.message}</button>
    </div>
  )
}

export default Button
  • 부모가 <Button message={"리액트"} color={"pink"} />로 쓴 속성들이 하나의 객체로 묶여서 자식 함수의 매개변수 props로 들어온다. props는 { message: "리액트", color: "pink" }이다.
  • 자식은 props.message, props.color처럼 꺼내 쓴다.
  • 값을 안 넘긴 <Button message={"DB"} />는 props.color가 undefined라서 기본 색으로 나온다. 렌더링해 보면 <button style="color:pink">리액트</button><button>DB</button>이다.
  • props는 읽기 전용이다. 자식이 props.color를 직접 바꾸면 안 되고, 바꿔야 한다면 부모의 상태를 바꿔서 다시 내려 줘야 한다.
  • 자식 컴포넌트를 쓰려면 import해야 한다.

구조 분해로 받기

props.color, props.message로 계속 쓰기 번거로우면 JS Day.7의 구조 분해를 매개변수 자리에서 쓴다. 기본값도 줄 수 있다.

const Button = ({ message, color = 'black' }) => {
  return <button style={{ color }}>{message}</button>
}

children

태그와 태그 사이에 적은 내용은 props.children으로 들어온다.

// 부모
<ColorPro color='pink' msg='첫 번째 문구' />
<ColorPro color='blue' msg='두 번째 문구' />
<ColorPro color='red'>태그 사이에 쓴 문구</ColorPro>
// 자식 (ColorPro.jsx)
const ColorPro = (props) => {
  console.log(props)

  const colorStyle = {
    color: props.color,
  }

  return (
    <div>
      <p style={colorStyle}>{props.msg}</p>
      <p style={{ color: props.color }}>{props.children}</p>
    </div>
  )
}

자식 컴포넌트가 부모에게서 받는 객체는 이렇게 된다.

{ color: 'pink', msg: '첫 번째 문구' }
{ color: 'blue', msg: '두 번째 문구' }
{ color: 'red', children: '태그 사이에 쓴 문구' }       ← 태그 사이의 내용이 children

세 번째처럼 msg 없이 태그 사이에 글을 쓴 경우는 props.msg가 undefined라서 첫 번째 <p>는 비어 있고, props.children이 있는 두 번째 <p>에만 글이 나온다. 렌더링해 보면 <p style="color:red"></p><p style="color:red">태그 사이에 쓴 문구</p>이다.

방식부모의 사용법자식에서 꺼내기
속성으로 전달<Comp msg="글" />props.msg
태그 사이로 전달<Comp>글</Comp>props.children

마무리

  • setState로 값을 바꾸면 컴포넌트가 다시 실행되어 화면이 갱신된다. 같은 이벤트 안에서 이전 값을 이어서 쓰려면 setNum(n => n + 1)처럼 함수로 넘긴다.
  • 스타일은 객체로, 속성 이름은 camelCase로 쓴다 (style={{ fontSize: '2em' }}).
  • 입력창은 onChange와 e.target.value로 상태에 저장하고, value={상태}로 연결하면 제어 컴포넌트가 된다.
  • 조건부 렌더링은 삼항 연산자, &&, ||를 쓴다. 숫자 0은 그려지므로 num > 0 && ...처럼 비교식으로 쓴다.
  • 리스트는 map으로 그리고, 각 요소에 고유한 key를 붙인다. 상태 배열은 push 대신 스프레드로 새 배열을 만들어 set 한다.
  • 가상 DOM은 바뀐 부분만 실제 DOM에 반영하는 방식이다.
  • props는 부모가 자식에게 넘기는 읽기 전용 값이고, 태그 사이의 내용은 children으로 받는다.

Tags: React useState props children 조건부렌더링 리스트렌더링 key 가상DOM 개발자 학습기록

profile
난 뭘까

0개의 댓글