useState로 값 바꾸기, 인라인 스타일, 입력창 다루기, 조건부 렌더링, 리스트 렌더링(map/key), 가상 DOM,props와children.
Day.1에서 useState로 초기값을 만들어 화면에 출력하는 데까지 했다. 오늘은 값을 실제로 바꾸고, 입력창과 연결하고, 조건이나 배열에 따라 화면을 다르게 그리는 것, 그리고 컴포넌트끼리 값을 전달하는 props를 배웠다.
setStateimport 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 …로 올라간다. 흐름은 이렇다.
onClickButton이 실행되어 setNum(num + 1)을 호출한다.setNum이 값을 바꾸고 컴포넌트를 다시 실행(리렌더링)한다.num은 새 값이고, <h2>가 새 값으로 그려진다.값을 바꿀 때는 num = num + 1처럼 변수를 직접 바꾸지 않고 반드시 setNum을 통해 바꾼다. 그래야 React가 "값이 바뀌었으니 다시 그려야 한다"는 것을 알 수 있다.
const [opinion, setOpinion] = useState(false)
const onOpinion = () => {
setOpinion(!opinion) // true ↔ false 뒤집기
}
<button onClick={onOpinion}>찬성/반대</button>
<h2>{opinion ? '찬성' : '반대'}</h2> {/* 삼항 연산자 */}
!opinion으로 값을 뒤집는다. 처음에는 false라서 반대가 보이고, 누를 때마다 찬성/반대가 번갈아 나온다.
문장 두 개를 각각 버튼으로 바꾸는 연습도 했다.
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)
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)로 넣어 준다.
JSX에서 style에는 문자열이 아니라 객체를 넣는다.
const con1 = {
color: 'red',
fontSize: '2em',
border: '3px solid red'
}
<h1 style={con1}>HI</h1>
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={함수()}로 쓰면 렌더링 때 바로 실행된다고 했던 이유로, 괄호가 필요한 호출은 () =>로 감싸야 클릭 때 실행된다.
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)라고 부른다.
const user = {
name: "홍길동",
isLogin: true,
}
<div>{user.isLogin == true ? '로그인' : '로그아웃'}</div>
// isLogin이 true면 '로그인', false면 '로그아웃'
JS Day.3의 삼항 연산자를 JSX 안의 { }에서 쓴 것이다. user.isLogin == true는 user.isLogin만 써도 같다.
&&와 ||const name = '리액트'
<div>
{name === '리액트' ? (<h1>리액트이다</h1>) : (<h1>리액트아니다</h1>)}
{name === '리액트' && (<h1>리액트이다</h1>)}
{name === '리엑트' || (<h1>리액트이다</h1>)}
</div>
| 연산자 | 규칙 | 용도 |
|---|---|---|
a ? b : c | a가 참이면 b, 아니면 c | 둘 중 하나를 보여 줄 때 |
a && b | a가 참이면 b를 반환 | 조건이 맞을 때만 보여 줄 때 |
a \|\| b | a가 거짓이면 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 && ...처럼 비교식으로 써야 한다.
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가 되고 입력창은 비워진다.필기에는 React가 화면을 갱신하는 과정이 세 단계로 적혀 있다.
가상 DOM: 실제 화면(DOM) 대신 메모리에 만들어 두는 가벼운 복사본.
실제 DOM을 매번 통째로 다시 그리지 않고, 바뀐 부분만 고치기 때문에 효율적이다. 위의 key도 이 비교 과정에서 쓰인다.
필기에는 "
render()가 다시 호출된다"고 적혀 있는데,render()는 클래스형 컴포넌트의 메소드 이름이다. 지금 쓰는 함수형 컴포넌트에서는 컴포넌트 함수 자체가 다시 호출되는 것이라고 이해하면 된다.
컴포넌트를 나누면 부모 컴포넌트가 자식 컴포넌트에게 값을 넘겨야 할 때가 생긴다. 이때 쓰는 것이 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.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)처럼 함수로 넘긴다.style={{ fontSize: '2em' }}).onChange와 e.target.value로 상태에 저장하고, value={상태}로 연결하면 제어 컴포넌트가 된다.&&, ||를 쓴다. 숫자 0은 그려지므로 num > 0 && ...처럼 비교식으로 쓴다.map으로 그리고, 각 요소에 고유한 key를 붙인다. 상태 배열은 push 대신 스프레드로 새 배열을 만들어 set 한다.children으로 받는다.Tags: React useState props children 조건부렌더링 리스트렌더링 key 가상DOM 개발자 학습기록