✅ JSX
javascript에서 XML을 확장한 문법입니다.
리액트에서 XML형식의 코드를 리턴할 수 있습니다.
브라우저에서 실행시 Bable도구를 사용하여 일반 Javascript 코드로 변환합니다.
📌 태그는 꼭 닫혀있어야 합니다.
📌 하나의 태그로 감싸야 합니다.
📌 JSX 내부에 자바스크립트 변수를 보여줘야 할 때는 {}
로 감싸서 보여줍니다.
📌 태그에 style과 className을 설정하는 방법
className="클래스명"
으로 설정function App() {
return (
<Hello name="react" color="red"/>
);
}
function Hello(props) {
return <div style={{ color: props.color }}>안녕하세요 {props.name}</div>
}
함수의 파라미터에서 비구조화 할당 문법을 사용하여 props를 더 간결하게 사용할 수 있습니다.
function Hello({ color, name }) {
return <div style={{ color }}>안녕하세요 {name}</div>
}
function Hello({ color, name }) {
return <div style={{ color }}>안녕하세요 {name}</div>
}
Hello.defaultProps = {
name: '이름없음'
}
function App() {
return (
<>
<Hello color="pink"/>
</>
);
}
//props로 name을 지정해주지 않으면 Hello컴포넌트에서 지정해준 defaultProps의 name이 렌더링
컴포넌트 태그 사이에 넣은 값을 조회하고 싶을 땐, props.children
을 조회하면 됩니다.
function Wrapper() {
return (
<div></div>
)
}
import Hello from './Hello';
import Wrapper from './Wrapper';
function App() {
return (
<Wrapper>
<Hello />
</Wrapper>
);
}
이때, Wrapper컴포넌트에서 props.children을 렌더링하지 않았으므로 <Hello />
컴포넌트가 보이지 않습니다.
Wrapper 내부의 내용이 보여지게 하기 위해서는 Wrapper 에서 props.children
을 렌더링해주어야 합니다.
function Wrapper({ children }) {
return (
<div>{children}</div>
)
}
✅ 조건부 렌더링
특정 조건에 따라 다른 결과물을 렌더링 하는 것
function App() {
return (
<Wrapper>
<Hello isSpecial={true}/>
//<Hello isSpecial /> props값을 생략하면 ={true}
</Wrapper>
)
}
function Hello({isSpecial}) {
return (
<div>
{isSpecial && <p>안녕</p>} //isSpecial이 true 일때만 <p>안녕</p>이 렌더링
</div>
);
}
import React, { useState } from 'react';
//React의 useState라는 함수로 컴포넌트의 상태를 관리한다.
function Counter() {
const [number, setNumber] = useState(0);
//number를 0으로 초기화
//setNumber(새로운값)로 number상태 변경
const onIncrease = () => {
setNumber(number + 1);
}
const onDecrease = () => {
setNumber(number - 1);
}
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
//주의!!
//함수 형태를 넣어주어야 한다.
//onClick={onIncrease()} 처럼 함수를 바로 실행시키면 안됨.
</div>
);
}
setter함수를 사용할때 업데이트 하고 싶은 새로운 값을 파라미터로 넣어주는 방식이 아니라, 기존 값을 어떻게 업데이트 할 지에 대한 함수를 등록하는 방식으로도 값을 업데이트 할 수 있습니다.
setNumber(prevNumber => prevNumber + 1);
function InputSample() {
const [text, setText] = useState('');
const onChange = (e) => {
setText(e.target.value);
};
//e.target은 input DOM을 가리킨다.
//Input에 값을 입력할때마다(onChange될때마다) text를 Input태그의 value값으로 상태업데이트
const onReset = () => {
setText('');
};
//onReset이 실행되면(버튼이 onClick되면) text=''이 되므로 Input안의 값이 지워진다
return (
<div>
<input onChange={onChange} value={text} />
//Input태그의 value를 text상태로 관리
//상태가 바뀌었을때 input 의 내용도 업데이트
<button onClick={onReset}>초기화</button>
<p>값: {text}</p>
</div>
);
}
function InputSample() {
const [inputs, setInputs] = useState({
name: '',
nickname: ''
});
const { name, nickname } = inputs; // 비구조화 할당을 통해 값 추출
const onChange = (e) => {
const { value, name } = e.target;
// 우선 e.target 에서 name 과 value 를 추출
setInputs({
...inputs, // 기존의 input 객체를 복사한 뒤 (전개연산자(spread 문법))
[name]: value // name 키를 가진 값을 value 로 설정
});
};
const onReset = () => {
setInputs({
name: '',
nickname: '',
})
};
return (
<div>
<input name="name" placeholder="이름" onChange={onChange} value={name} />
<input name="nickname" placeholder="닉네임" onChange={onChange} value={nickname}/>
<button onClick={onReset}>초기화</button>
<div>
<b>값: </b>
{name} ({nickname})
</div>
</div>
);
}
🌟 리액트 상태에서 '객체'를 수정해야 할 때에는 객체를 직접 수정하면 안되고, 새로운 객체를 만들어서 그 객체에 변화를 주고 이를 상태로 사용해주어야 합니다.
이러한 작업을, "🌟불변성을 지킨다🌟 라고 부릅니다. 불변성을 지켜주어야만 리액트 컴포넌트에서 상태가 업데이트가 됐음을 감지 할 수 있고 이에 따라 필요한 리렌더링이 진행됩니다.
❌inputs[name] = value;
이렇게 객체를 직접 수정하면 값을 바꿔도 리렌더링이 되지 않습니다.
setInputs({
...inputs,
[name]: value
});
리액트를 사용하는 프로젝트에서 DOM 을 직접 선택해야 할 때 ref
라는 것을 사용합니다. 함수형 컴포넌트에서 ref 를 사용 할 때에는 useRef 라는 Hook 함수를 사용합니다.
function InputSample() {
const nameInput = useRef(); //useRef() 를 사용하여 Ref 객체를 만든다.
const onReset = () => {
nameInput.current.focus();
//Ref객체의 .current는 우리가 원하는 DOM(밑에서 설정해준 input) 을 가리키게 됩니다.
};
return (
<div>
<input
placeholder="이름"
ref={nameInput} //nameInput객체를 이 DOM에 ref 값으로 설정
/>
<button onClick={onReset}>초기화</button>
</div>
);
}
📌동적인 배열을 렌더링해야 할 때에는 자바스크립트 배열의 내장함수 map()
을 사용합니다.
📌리액트에서 배열을 렌더링 할 때에는 key 라는 props 를 설정해야합니다. key 값은 각 원소들마다 가지고 있는 고유값으로 설정을 해야합니다.
const array = ['a', 'b', 'c', 'd'];
위 배열을 렌더링한다고 하면,
array.map(item => <div>{item}</div>);
이 때 b 와 c 사이에 z 를 삽입하게 된다면, 리렌더링을 하게 될 때 <div>b</div>
와 <div>c</div>
사이에 새 div 태그를 삽입을 하게 되는 것이 아니라, 기존의 c 가 z 로바뀌고, d 는 c 로 바뀌고, 맨 마지막에 d 가 새로 삽입됩니다. => 비효율적!!
[ { id: 0, text: 'a' }, { id: 1, text: 'b' }, { id: 2, text: 'c' }, { id: 3, text: 'd' } ];
위 배열을 렌더링 할때 고유한 값인 id를 key props으로 전달해주면,
array.map(item => <div key={item.id}>{item.text}</div>);
배열이 업데이트 될 떄 key 가 없을 때 처럼 비효율적으로 업데이트 하는 것이 아니라, 수정되지 않는 기존의 값은 그대로 두고 원하는 곳에 내용을 삽입하거나 삭제합니다.
useRef Hook 은 DOM 을 선택하는 용도 외에도, 다른 용도가 한가지 더 있는데요, 바로, 컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리하는 것 입니다.
useRef 로 관리하는 변수는 값이 바뀐다고 해서 컴포넌트가 리렌더링되지 않습니다. 따라서 useRef 로 관리하고 있는 변수는 설정 후 바로 조회 할 수 있습니다.
이 변수를 사용하여 다음과 같은 값을 관리 할 수 있습니다.
useRef() 를 사용 할 때 파라미터를 넣어주면, 이 값이 .current 값의 기본값이 됩니다. 그리고 이 값을 수정 할때에는 .current 값을 수정하면 되고 조회 할 때에는 .current 를 조회하면 됩니다.
import React from "react";
function User({ user, onRemove, onToggle }) {
return (
<div>
<b
style={{
cursor: "pointer",
color: user.active ? "green" : "black"
}}
onClick={() => onToggle(user.id)}
>
{user.username}
</b>
<span>({user.email})</span>
<button onClick={() => onRemove(user.id)}>삭제</button>
</div>
);
}
export default User;
import React from "react";
import User from "./User";
function UserList({ users, onRemove, onToggle }) {
return (
<div>
{users.map((user) => (
<User
user={user}
key={user.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
}
export default UserList;
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;
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
});
}; //Input태그에 입력할때마다 inputs상태 업데이트
const [users, setUsers] = useState([
{
id: 1,
username: "user1",
email: "user1@gmail.com",
active: true
},
{
id: 2,
username: "user2",
email: "user2@gmail.com",
active: false
},
{
id: 3,
username: "user3",
email: "user3@gmail.com",
active: false
}
]);
const nextId = useRef(4);
const onCreate = () => {
const user = {
id: nextId.current,
username,
email
};
setUsers(users.concat(user));
//전개연산자 외에 concat을 쓰는 방법도 있습니다.
//concat 함수는 기존의 배열을 수정하지 않고, 새로운 원소가 추가된 새로운 배열을 만들어줍니다.
setInputs({
username: "",
email: ""
}); //추가 후 Inputs상태 초기화
nextId.current += 1;
}; //'등록'버튼 클릭시 setUsers(newArr), setInputs로 inputs 초기화
const onRemove = (id) => {
setUsers(users.filter((user) => user.id !== id));
};//'삭제'버튼 클릭시 해당 User컴포넌트의 id를 받아와서 삭제
const onToggle = (id) => {
setUsers(
users.map((user) =>
user.id === id ? { ...user, active: !user.active } : user
)
);
};//이름 클릭시 해당 User컴포넌트의 id를 받아와서 active상태 바꿈
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
</>
);
}
export default App;
✅ useEffect는 컴포넌트가 마운트, 언마운트, 업데이트 될 때 특정 함수를 실행할 수 있는 Hook입니다.
import React, { useEffect } from 'react';
function User({ user, onRemove, onToggle }) {
useEffect(() => {
console.log('컴포넌트가 화면에 나타남');
return () => {
console.log('컴포넌트가 화면에서 사라짐');
};
}, [user]);
return (
<div>
<b
style={{
cursor: "pointer",
color: user.active ? "green" : "black"
}}
onClick={() => onToggle(user.id)}
>
{user.username}
</b>
<span>({user.email})</span>
<button onClick={() => onRemove(user.id)}>삭제</button>
</div>
);
}
export default User;
⭐useEffect의 두번째 파라미터는 의존성배열입니다. 의존성배열(deps)안의 특정 값이 바뀔때마다 Hook이 호출되고, 의존성배열이 비어있는 경우 컴포넌트가 최초 렌더링될때 한번만 호출됩니다.
⭐useEffect가 반환하는 함수를 cleanUp 함수라 부릅니다. useEffect 에 대한 뒷정리를 해준다고 이해하시면 되는데요, deps 가 비어있는 경우에는 컴포넌트가 사라질 때 cleanup 함수가 호출됩니다.
⭐deps 파라미터를 생략한다면, 컴포넌트가 리렌더링 될 때마다 호출이 됩니다.
주로, 마운트 시에 하는 작업들
- props 로 받은 값을 컴포넌트의 로컬 상태로 설정
- 외부 API 요청 (REST API 등)
- 라이브러리 사용 (D3, Video.js 등...)
- setInterval 을 통한 반복작업 혹은 setTimeout 을 통한 작업 예약
언마운트 시에 하는 작업들
- setInterval, setTimeout 을 사용하여 등록한 작업들 clear 하기 (clearInterval, clearTimeout)
- 라이브러리 인스턴스 제거
✅ useMemo는 두번째 파라미터인 배열 안에 넣은 내용이 바뀌면 등록한 함수를 호출해서 값을 연산해주고, 바뀌지 않았다면 이전에 연산한 값을 재사용하여 성능최적화를 할 수 있는 Hook입니다.
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
function App() {
...
//const count = countActiveUsers(users);
//App안의 다른 상태가 바뀔때마다 countActiveUsers함수가 계속 호출됩니다.
const count = useMemo(() => countActiveUsers(users), [users]);
//useMemo를 쓰면 users상태가 바뀔때에만 호출
return (<>...</>);
}
✅ useCallback 은 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용합니다.
import React, { useRef, useState, useMemo, useCallback } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
function App() {
const onChange = useCallback(
e => {
const { name, value } = e.target;
setInputs({
...inputs,
[name]: value
});
},
[inputs]
);
...
const onCreate = useCallback(함수, [users, username, email]);
const onRemove = useCallback(함수, [users]);
const onToggle = useCallback(함수, [users]);
const count = useMemo(() => countActiveUsers(users), [users]);
return (<>...</>);
}
export default App;
주의 하실 점은, 함수 안에서 사용하는 상태 혹은 props 가 있다면 꼭, deps 배열안에 포함시켜야 된다는 것 입니다. 만약에 deps 배열 안에 함수에서 사용하는 값을 넣지 않게 된다면, 함수 내에서 해당 값들을 참조할때 가장 최신 값을 참조 할 것이라고 보장 할 수 없습니다. props 로 받아온 함수가 있다면, 이 또한 deps 에 넣어주어야 해요.
✔ useCallback은 useMemo를 기반으로 만들어졌기 때문에 다음과같이 쓸 수도 있습니다.
const onToggle = useMemo(
() => () => {
/* ... */
},
[users]
);
✅ React.memo는 컴포넌트의 props 가 바뀌지 않았다면, 리렌더링을 방지하여 컴포넌트의 리렌더링 성능 최적화를 해줄 수 있는 함수입니다.
const CreateUser = ({ username, email, onChange, onCreate }) => {
return (...);
};
export default React.memo(CreateUser); // => 사용법은, 그냥 감싸주면 됩니다!
const User = function User({ user, onRemove, onToggle }) {
return (...);
};
export default React.memo(User)
function UserList({ users, onRemove, onToggle }) {
return (...);
}
export default React.memo(UserList);
이렇게 하면 input이 onChange될 때 하단의 userList는 렌더링되지 않습니다.
그런데, User 중 하나라도 수정하면 모든 User 들이 리렌더링되고, CreateUser 도 리렌더링이 됩니다. 이는 onCreate, onToggle,onRemove 의 useCallback dept에 users가 있어서 함수가 새로 만들어지기 때문입니다.
=> 이걸 최적화하고 싶다면? 햠수형 업데이트!!
deps 에서 users 를 지우고, 함수들에서 현재 useState 로 관리하는 users 를 참조하지 않게 하는것입니다. 함수형 업데이트를 하게 되면, setUsers 에 등록하는 콜백함수의 파라미터에서 최신 users 를 참조 할 수 있기 때문에 deps 에 users 를 넣지 않아도 된답니다.
import React, { useRef, useState, useMemo, useCallback } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
function App() {
const [inputs, setInputs] = useState({
username: '',
email: ''
});
const { username, email } = inputs;
const onChange = useCallback(e => {
const { name, value } = e.target;
setInputs(inputs => ({
...inputs,
[name]: value
})); //함수형 업데이트!!
}, []);
const [users, setUsers] = useState([
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: false
},
{
id: 3,
username: 'liz',
email: 'liz@example.com',
active: false
}
]);
const nextId = useRef(4);
const onCreate = useCallback(() => {
const user = {
id: nextId.current,
username,
email
};
setUsers(users => users.concat(user)); //함수형 업데이트!!
setInputs({
username: '',
email: ''
});
nextId.current += 1;
}, [username, email]);
const onRemove = useCallback(id => {
setUsers(users => users.filter(user => user.id !== id));
}, []); //함수형 업데이트!!
const onToggle = useCallback(id => {
setUsers(users =>
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
); //함수형 업데이트!!
}, []);
const count = useMemo(() => countActiveUsers(users), [users]);
return (...);
}
export default App;
추가적으로, React.memo 에서 두번째 파라미터에 propsAreEqual 이라는 함수를 사용하여 특정 값들만 비교를 하는 것도 가능합니다.
export default React.memo(
UserList,
(prevProps, nextProps) => prevProps.users === nextProps.users
);
useReducer Hook 함수를 사용하면 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있습니다.
✅ reducer 는 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수입니다.
function reducer(state, action) {
// 새로운 상태를 만드는 로직
// const nextState = ...
return nextState;
}
✅ action 은 업데이트를 위한 정보를 가지고 있습니다. 주로 type 값을 지닌 객체 형태로 사용
//action 객체 예시
{
type: 'ADD_TODO',
todo: {
id: 1,
text: 'useReducer 배우기',
done: false,
}
}
✅ useReducer 사용법
const [state, dispatch] = useReducer(reducer, initialState);
//state: 컴포넌트에서 사용할 수 있는 상태
//dispatch: 액션을 발생시키는 함수. 다음과 같이 사용합니다: dispatch({ type: 'INCREMENT' })
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
function Counter() {
const [number, dispatch] = useReducer(reducer, 0);
const onIncrease = () => {
dispatch({ type: 'INCREMENT' });
}; //dispatch함수로 action만 전달해주면 reducer함수에서 알아서 상태 업데이트
const onDecrease = () => {
dispatch({ type: 'DECREMENT' });
}; //dispatch함수로 action만 전달해주면 reducer함수에서 알아서 상태 업데이트
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
}
export default Counter;
✅ 비슷한 코드가 반복될때에는 커스텀 Hooks 를 만들어서 반복되는 로직을 쉽게 재사용할 수 있습니다.
✅ 커스텀 Hooks 를 만들 때에는 보통 use 라는 키워드로 시작하는 파일을 만들고 그 안에 함수를 작성합니다.
✅ 커스텀 Hooks 를 만드는 방법: 안에서 useState, useEffect, useReducer, useCallback 등 Hooks 를 사용하여 원하는 기능을 구현해주고, 컴포넌트에서 사용하고 싶은 값들을 반환해주면 됩니다.
//커스텀 훅 예시
function useInputs(initialForm) {
const [form, setForm] = useState(initialForm);
const onChange = useCallback(e => {
const { name, value } = e.target;
setForm(form => ({ ...form, [name]: value }));
}, []);
const reset = useCallback(() => setForm(initialForm), [initialForm]);
return [form, onChange, reset];
}
export default useInputs;
//App에서 사용
function App() {
const [{ username, email }, onChange, reset] = useInputs({
username: '',
email: ''
});
const [state, dispatch] = useReducer(reducer, initialState);
...
프로젝트에서 상태를 전역적으로 사용할 수 있는 값을 관리하는 API
// UserDispatch 라는 이름으로 내보내줍니다.
export const UserDispatch = React.createContext(null);
function App() {
...
const [state, dispatch] = useReducer(reducer, initialState);
...
return (
<UserDispatch.Provider value={dispatch}>
...
</UserDispatch.Provider>
);
}
export default App;
UserDispatch 라는 Context 를 만들어서, 어디서든지 dispatch 를 꺼내 쓸 수 있도록 준비를 해준 것입니다.
이제 onRemove, onToggle등의 함수를 props로 전달, 전달 받지 않고 User컴포넌트에서 바로 dispatch해줄 수 있습니다.
import React, { useContext } from 'react';
import { UserDispatch } from './App';
const User = React.memo(function User({ user }) {
const dispatch = useContext(UserDispatch);
//UserDispatch라는 context를 사용하겠다
return (
<div>
<b
style={{
cursor: 'pointer',
color: user.active ? 'green' : 'black'
}}
onClick={() => {
dispatch({ type: 'TOGGLE_USER', id: user.id });
}} //App에서 UserDispatch.Provider로 전달해준 value={dispatch} 를 사용해서 dispatch함수를 실행
>
{user.username}
</b>
<span>({user.email})</span>
<button
onClick={() => {
dispatch({ type: 'REMOVE_USER', id: user.id });
}}//App에서 UserDispatch.Provider로 전달해준 value={dispatch} 를 사용해서 dispatch함수를 실행
>
삭제
</button>
</div>
);
});