[React Native] 처음 배우는 리액트 네이티브 6장

나경·2025년 1월 22일

Hooks을 사용하면 함수형 클래스형 컴포넌트가 아닌 함수형 컴포넌트에서도 상태를 관리할 수 있다

useState

useState 함수를 호출하면 변수와 그 변수를 수정할 수 있는 세터 함수를 배열로 반환

const [state, useState] = useState(initialState);
  1. 상태를 관리해야 하는 변수는 반드시 세터 함수를 이용해서 값을 변경해야 한다
  2. 상태가 변경되면 컴포넌트가 변경된 내용을 반영해서 다시 렌더링한다

useState 사용하기

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

컴포넌트가 렌더링될 때마다 원하는 작업이 실행되도록 설정할 수 있다

useEffect(() => {}, []);
  • 첫 번째 파라미터 함수 : 조건 만족할 때마다 호출
  • 두 번째 파라미터 배열 : 함수 호출 조건 설정

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 컴포넌트의 렌더링 상태를 변경한다

useRef

특정 컴포넌트를 선택해야 하는 경우에 사용

const ref = useRef(initialValue);

주의할 점

  1. 컴포넌트의 ref로 지정하면 생성된 변수에 값이 저장되는 것이 아니라 변수의 .current 프로퍼티에 해당 값을 담는다
  2. useRef의 내용이 변경돼도 컴포넌트 다시 렌더링되지 않는다 (useState와의 차이)
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

동일한 연산의 반복 수행을 제거해서 성능을 최적화하는 데 사용한다

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

커스텀 Hooks 만들기

특정 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


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

0개의 댓글