Hooks을 사용하면 함수형 클래스형 컴포넌트가 아닌 함수형 컴포넌트에서도 상태를 관리할 수 있다
useState 함수를 호출하면 변수와 그 변수를 수정할 수 있는 세터 함수를 배열로 반환
const [state, useState] = useState(initialState);
Button 컴포넌트를 사용해서 클릭될 때마다 세터 함수를 통해서 상태를 변경한다
// App.js
import React from 'react';
import styled from 'styled-components/native';
import Counter from './components/Counter';
const Container = styled.View`
flex: 1;
background-color: #fff;
justify-content: center;
align-items: center;
`
const App = () => {
return (
<Container>
<Counter />
</Container>
)
}
export default App
// Button.js
import React from 'react'
import styled from 'styled-components/native'
const Container = styled.TouchableOpacity`
background-color: #3498db;
border-radius: 15px;
padding: 15px 30px;
margin: 10px 0px;
justify-content: center;
`
const Title = styled.Text`
font-size: 24px;
font-weight: 600;
color: #ffffff;
`
const Button = ({title, onPress}) => {
return (
<Container onPress={onPress}>
<Title>{title}</Title>
</Container>
)
}
export default Button
// Counter.js
import React, { useState } from 'react';
import styled from 'styled-components/native';
import Button from './Button';
const StyledText = styled.Text`
font-size: 24px;
margin: 10px;
`
const Counter = () => {
const [count, setCount] = useState(0);
return (
<>
<StyledText>count: {count}</StyledText>
<Button
title="+"
onPress={() => {
setCount(count+1);
}}
/>
<Button
title="-"
onPress={() => {
setCount(count-1);
}}
/>
</>
)
}
export default Counter

세터 함수를 사용하는 방법
1. 세터 함수에 변경된 값을 전달 (지금까지 사용했던 방법)
2. 세터 함수의 파라미터에 함수를 전달
세터 함수는 비동기로 동작하기 때문에
여러 번 상태 변경이 일어나면 상태가 변경되기 전에 또 다시 상태에 대한 업데이트가 실행된다
// 1번 코드
<Button
title="+"
onPress={() => {
setCount(count+1);
setCount(count+1);
}}
/>
// 2번 코드
<Button
title="+"
onPress={() => {
setCount(prevCount => prevCount+1);
setCount(prevCount => prevCount+1);
}}
/>
따라서 1번 코드는 버튼을 클릭하면 1씩 증가하지만
2번 코드는 버튼을 클릭하면 2씩 증가한다
컴포넌트가 렌더링될 때마다 원하는 작업이 실행되도록 설정할 수 있다
useEffect(() => {}, []);
import React, { useEffect, useState } from 'react'
import styled from 'styled-components/native';
const StyledTextInput = styled.TextInput.attrs({
autoCapitalize: 'none',
autoCorrext: false,
})`
border: 1px solid #757575;
padding: 10px;
margin: 10px 0px;
width: 200px;
font-size: 20px;
`
const StyledText = styled.Text`
font-size: 24px;
margin: 10px;
`
const Form = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
useEffect(() => {
console.log(`name: ${name}, email: ${email}\n`);
})
return (
<>
<StyledText>Name: {name}</StyledText>
<StyledText>Email: {email}</StyledText>
<StyledTextInput
value={name}
onChangeText={text => setName(text)}
placeholder="name"
/>
<StyledTextInput
value={email}
onChangeText={text => setEmail(text)}
placeholder="email"
/>
</>
)
}
export default Form

값이 변경될 때마다 useEffect에 전달한 함수가 잘 실행된다
만약 특정 상태가 변경될 때만 호출하고 싶다면
useEffect의 두 번째 파라미터에 해당 상태를 관리하는 변수를 배열로 전달하면 된다
만약 email이 변경될 때만 useEffect가 동작하게 하고 싶다면?
useEffect(() => {
console.log(`name: ${name}, email: ${email}\n`);
},[email])

이제 email의 상태가 변경될 때만 함수가 실행된다
name 상태가 변할 때는 함수 호출 x
두 번째 파라미터에 빈 배열을 전달하면 컴포넌트가 마운트될 때 useEffect에 전달된 함수가 실행된다
useEffect(() => {
console.log(`\n=== Form Component Mount ==\n`);
},[])
useEffect(() => {
console.log(`name: ${name}, email: ${email}\n`);
},[email])

첫 번째 useEffect는 컴포넌트가 처음 렌더링될 때 함수를 호출하고, 이후에는 컴포넌트가 다시 렌더링되어도 함수 실행 x
정리 함수: useEffect에서 전달하는 함수에서 반환하는 함수
useEffect의 실행 조건이 빈 배열인 경우 컴포넌트가 언마운트될 때 정리 함수를 실행하면 된다
useEffect(() => {
console.log(`\n=== Form Component Mount ==\n`);
return () => console.log('\n === Form Component Unmout === \n')
},[])
const App = () => {
const [isVisible, setIsVisible] = useState(true);
return (
<Container>
<Button
title={isVisible ? 'Hide' : 'Show'}
onPress={() => setIsVisible(prev => !prev)}
/>
{isVisible && <Form />}
</Container>
)
}
export default App
버튼을 클릭할 때마다 Form 컴포넌트의 렌더링 상태를 변경한다
특정 컴포넌트를 선택해야 하는 경우에 사용
const ref = useRef(initialValue);
주의할 점
const Form = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const refName = useRef(null);
const refEmail = useRef(null);
useEffect(() => {
console.log(`\n=== Form Component Mount ==\n`);
// 컴포넌트가 마운트될 때 포커스가 이름 입력하는 컴포넌트에 있도록
refName.current.focus();
return () => console.log('\n === Form Component Unmout === \n')
},[])
return (
<>
<StyledText>Name: {name}</StyledText>
<StyledText>Email: {email}</StyledText>
<StyledTextInput
value={name}
onChangeText={text => setName(text)}
placeholder="name"
ref={refName}
returnKeyType="next"
onSubmitEditing={() => refEmail.current.focus()} // Enter 입력시 함수 호출
/>
<StyledTextInput
value={email}
onChangeText={text => setEmail(text)}
placeholder="email"
ref={refEmail}
returnKeyType="done"
/>
</>
)
}
export default Form
위의 코드는 컴포넌트로 포커스를 설정하기 위해서 해당하는 컴포넌트를 선택하려고 useRef를 사용했다

동일한 연산의 반복 수행을 제거해서 성능을 최적화하는 데 사용한다
useMemo(() => {}, []);
import React, { useState } from 'react';
import styled from 'styled-components/native';
import Button from './Button';
const StyledText = styled.Text`
font-size: 24px;
`
const getLength = text => {
console.log(`Target Text: ${text}`);
return text.length
}
const list = ['JacaScript', 'Expo', 'Expo', 'React Native'];
let idx = 0;
const Length = () => {
const [text, setText] = useState(list[0]);
const [length, setLength] = useState('');
const _onPress = () => {
setLength(getLength(text));
++idx;
if (idx < list.length) setText(list[idx]);
}
return (
<>
<StyledText>Text: {text}</StyledText>
<StyledText>Length: {length}</StyledText>
<Button title="Get Length" onPress={_onPress}/>
</>
)
}
export default Length
버튼을 클릭하면 배열을 순환하며 문자열의 길이를 구한다

여기서 useMemo를 사용하면 계산하는 값에 변화가 있는 경우에만 함수가 호출된다 불필요한 연산 제거 가능!
import React, { useMemo, useState } from 'react';
import styled from 'styled-components/native';
import Button from './Button';
const StyledText = styled.Text`
font-size: 24px;
`
const getLength = text => {
console.log(`Target Text: ${text}`);
return text.length
}
const list = ['JacaScript', 'Expo', 'Expo', 'React Native'];
let idx = 0;
const Length = () => {
const [text, setText] = useState(list[0]);
const _onPress = () => {
++idx;
if (idx < list.length) setText(list[idx]);
}
const length = useMemo(() => getLength(text), [text]);
return (
<>
<StyledText>Text: {text}</StyledText>
<StyledText>Length: {length}</StyledText>
<Button title="Get Length" onPress={_onPress}/>
</>
)
}
export default Length

특정 API에 GET 요청을 보내고 응답을 받는 함수를 구현해보겠다
// useFetch.js
import { useEffect, useState } from "react"
export const useFetch = url => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url);
const result = await res.json();
if (res.ok) {
setData(result);
setError(null);
} else {
throw result;
}
} catch (error) {
setError(error);
}
};
fetchData();
},[]);
return {data, error};
}
// 무작위로 강아지 사진 받아오는 컴포넌트
import React from 'react'
import styled from 'styled-components/native'
import { useFetch } from '../hooks/useFetch'
const StyledImage = styled.Image`
background-color: #7f8c8d;
width: 300px;
height: 300px;
`
const ErrorMessage = styled.Text`
font-size: 18px;
color: #e74c3c;
`
const URL = 'https://dog.ceo/api/breeds/image/random';
const Dog = () => {
const {data, error} = useFetch(URL);
return (
<>
<StyledImage source={data?.message ? {uri: data.message} : null}/>
<ErrorMessage>{error?.message}</ErrorMessage>
</>
)
}
export default Dog
이 때 비동기 함수를 사용해야 한다면
useEffect에 전달되는 함수 내부에 비동기 함수를 정의해야 한다

API 요청을 보내는 비동기 동작에서는 선행된 작업이 마무리되기 전에는 추가 요청이 들어오지 않는 것이 좋다
// useFetch.js
import { useEffect, useState } from "react"
export const useFetch = url => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [inProgress, setInProgress] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
setInProgress(true);
const res = await fetch(url);
const result = await res.json();
if (res.ok) {
setData(result);
setError(null);
} else {
throw result;
}
} catch (error) {
setError(error);
} finally {
setInProgress(false);
}
};
fetchData();
},[]);
return {data, error, inProgress};
}
import React from 'react'
import styled from 'styled-components/native'
import { useFetch } from '../hooks/useFetch'
const StyledImage = styled.Image`
background-color: #7f8c8d;
width: 300px;
height: 300px;
`
const ErrorMessage = styled.Text`
font-size: 18px;
color: #e74c3c;
`
const LoadingMessage = styled.Text`
font-size: 18px;
color: #2ecc71;
`
const URL = 'https://dog.ceo/api/breeds/image/random';
const Dog = () => {
const {data, error, inProgress} = useFetch(URL);
return (
<>
{inProgress && (
<LoadingMessage>The API request is in progress</LoadingMessage>
)}
<StyledImage source={data?.message ? {uri: data.message} : null}/>
<ErrorMessage>{error?.message}</ErrorMessage>
</>
)
}
export default Dog

참고
처음 배우는 리액트 네이티브