리액트 네이티브 강좌. 9편 - Input and Forms with Switch, KeyboardAvoidingView, Form Validation

안녕하세요!

리액트 네이티브 강좌 9편입니다.

편의를 위해 전체 강좌 링크도 넣었습니다.

  1. 리액트 네이티브 강좌. 1편 - 소개 및 Expo 살펴보기

  2. 리액트 네이티브 강좌. 2편 - 핵심 컴포넌트 가이드 - 뷰, 텍스트, 이미지부터 커스텀 컴포넌트까지

  3. 리액트 네이티브 강좌. 3편 - 스타일링 - 스타일시트, 박스 모델, 그림자, 그리고 상속

  4. 리액트 네이티브 강좌. 4편 - Flexbox - 기본 개념과 flexDirection 설정

  5. 리액트 네이티브 강좌. 5편 - Flexbox - 정렬 및 레이아웃 속성

  6. 리액트 네이티브 강좌. 6편 - Flexbox - basis, shrink, grow 및 레이아웃 전략

  7. 리액트 네이티브 강좌. 7편 - 동적인 사용자 인터페이스 구현하기 - Dimensions API와 플랫폼별 코드 작성법

  8. 리액트 네이티브 강좌. 8편 - 리스트 렌더링하기 그리고 FlatList, SectionList 사용하기

  9. 리액트 네이티브 강좌. 9편 - Input and Forms with Switch, KeyboardAvoidingView, Form Validation

  10. 리액트 네이티브 강좌. 10편 - Networking 다루기, 데이터 fetching, loading state, error handling


** 목 차 **


안녕하세요!

이번에는 React Native에서 입력과 폼에 대해 집중적으로 다룰 예정입니다.

웹 애플리케이션 UI를 개발할 때는 사용자 입력을 받을 수 있는 많은 HTML 요소들이 있습니다.

예를 들어, 입력 필드, 텍스트 영역, 드롭다운 메뉴, 체크박스, 라디오 버튼 등 다양한 요소들이 있죠.

하지만 React Native를 사용할 때는 선택지가 훨씬 제한적입니다.

React Native의 기본 라이브러리는 두 가지 입력 컴포넌트만 제공합니다: TextInput과 Switch입니다.

이 두 가지 컴포넌트에 집중할 것입니다.

그럼 다른 컴포넌트들은 어떻게 할까요?

Expo가 체크박스나 데이트 피커와 같은 추가 컴포넌트를 제공한다는 것입니다.

이러한 주제들은 나중에 다룰 예정입니다.

React Native에서 폼을 구현하는 것은 또 다른 도전 과제입니다.

우리는 보통 폼 상태 관리, 폼 검증 처리, 검증 메시지 표시, 폼 데이터 제출이라는 네 가지 주요 측면에 집중합니다.

이러한 작업들은 React Hook Form 같은 라이브러리를 사용하면 간소화될 수 있지만, 이번에는 외부 의존성 없이 순수한 React Native로 폼을 다루는 방법을 보여드리겠습니다.

나중에 React Native와 React Hook Form을 결합하는 방법에 대해서도 알아볼 예정입니다.


TextInput

이번에는 React Native에서 텍스트 입력 컴포넌트에 대해 알아보겠습니다.

텍스트 입력 컴포넌트는 React Native에서 사용자 입력을 받기 위한 기본적인 블록입니다.

이를 통해 사용자들은 애플리케이션에 텍스트와 기타 데이터를 입력할 수 있습니다.

코드를 통해 더 자세히 알아보도록 하겠습니다.

먼저 app.js 파일에 몇 가지 변경을 해보겠습니다.

Expo에서 가져온 status bar import를 제거하고 대신 React Native에서 가져옵니다.

Expo SDK 섹션에 도달하기 전까지는 가능한 한 순수한 React Native만 사용하도록 하겠습니다.

다음으로 View 컴포넌트를 SafeAreaView로 교체합니다.

이렇게 하면 iOS에서 상단의 패딩을 처리할 수 있습니다.

Android의 경우, status bar 높이만큼 패딩을 추가해보겠습니다.

container 스타일에서 paddingTop을 statusBar.currentHeight로 설정합니다.

텍스트 입력이 배치되는 방식에 영향을 미치는 정렬 속성들도 제거합니다.

마지막으로 SafeAreaView 안의 jsx를 모두 삭제합니다.

이제 텍스트 입력을 위한 준비가 완료되었습니다.

React Native에서 TextInput을 import합니다.

import { StyleSheet, StatusBar, SafeAreaView, TextInput } from "react-native";

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <TextInput />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    paddingTop: StatusBar.currentHeight,
  },
});

SafeAreaView 내에서 컴포넌트를 호출하고 파일을 저장한 후 디바이스에서 확인해봅니다.

지금은 텍스트 입력 컴포넌트가 보이지 않지만, 화면에는 존재합니다.

스타일링이 되어 있지 않아서 보이지 않는 것뿐입니다.

입력을 클릭해보면 커서가 나타나는 것을 볼 수 있습니다.

이제 VS Code로 돌아가 스타일을 추가해보겠습니다.

키를 input으로 설정하고 높이를 40 픽셀로 지정합니다.

margin, padding, 그리고 border width를 추가합니다.

이 스타일을 TextInput 컴포넌트에 할당합니다.

style={styles.input}으로 설정합니다.

import { StyleSheet, StatusBar, SafeAreaView, TextInput } from "react-native";

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <TextInput style={styles.input} />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    paddingTop: StatusBar.currentHeight,
  },
  input: {
    height: 40,
    margin: 12,
    padding: 10,
    borderWidth: 1,
  },
});

이제 UI를 확인해보면 텍스트 입력 컴포넌트가 명확하게 보입니다.

입력에 집중하고 타이핑을 시작할 수 있습니다.

Android 디바이스에서도 잘 작동합니다.

타이핑을 계속하거나 키보드를 사용할 수 있습니다.

둘 다 잘 작동합니다.

iOS의 경우 비슷한 키보드를 원한다면 command+shift+K를 눌러 키보드를 띄울 수 있습니다.

다시 command+shift+K를 누르면 키보드가 사라집니다.

지금 입력한 값은 추적되지 않습니다.

입력 값을 추적하려면 상태 변수를 사용해야 합니다.

React에서 useState를 import하고 상태 변수를 만듭니다.

변수 이름은 name, 함수는 setName, 초기 값은 빈 문자열로 설정합니다.

이제 name을 text input의 value prop에 할당하고 onChangeText prop에 setName 함수를 할당합니다.

이렇게 하면 입력 상자에 텍스트를 입력할 때 상태 변수가 자동으로 업데이트됩니다.

입력 값을 추적하고 있는지 확인하기 위해 텍스트를 추가해보겠습니다.

텍스트로 "My name is" 뒤에 name 상태 변수를 추가합니다.

명확하게 하기 위해 스타일도 추가합니다.

style={styles.text}로 설정합니다.

텍스트 스타일을 정의하고 폰트 크기를 30으로, 패딩을 10으로 설정합니다.

import { StyleSheet, Text, StatusBar, SafeAreaView, TextInput } from "react-native";
import { useState } from "react";

export default function App() {
  const [name, setName] = useState("");

  return (
    <SafeAreaView style={styles.container}>
      <TextInput style={styles.input} value={name} onChangeText={setName} />
      <Text style={styles.text}>My name is {name}</Text>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    paddingTop: StatusBar.currentHeight,
  },
  input: {
    height: 40,
    margin: 12,
    padding: 10,
    borderWidth: 1,
  },
  text: {
    fontSize: 30,
    padding: 10,
  },
});

이제 디바이스로 돌아가서 타이핑을 시작하면 입력 값이 아래의 텍스트 컴포넌트에 반영되는 것을 볼 수 있습니다.

요약하자면, 텍스트 입력 컴포넌트는 키보드를 통해 앱에 텍스트를 입력하기 위한 도구입니다.

위 예제에서는 TextInput 컴포넌트를 사용하여 사용자의 이름을 입력받고, 입력된 이름을 Text 컴포넌트에 표시합니다.

useState를 사용하여 입력 값을 관리하고, onChangeText 속성을 통해 입력 값이 변경될 때 상태를 업데이트합니다.

이제 이 코드를 실행하면, 입력 필드에 이름을 입력할 수 있고, 입력한 내용이 아래의 텍스트로 실시간으로 반영되는 것을 볼 수 있습니다.


TextInput Props

이제까지 React Native에서 텍스트 입력 컴포넌트의 기본적인 측면을 살펴보았습니다.

이번에는 텍스트 입력 컴포넌트의 동작과 외관을 사용자 맞춤 설정할 수 있는 중요한 props에 대해 알아보겠습니다.

Placeholder

첫 번째로 살펴볼 prop은 placeholder입니다.

이것은 사용자가 입력해야 할 내용에 대한 시각적 힌트를 제공합니다.

예를 들어, placeholder를 "email@example.com"으로 설정할 수 있습니다.

<TextInput
    style={styles.input}
    value={name}
    onChangeText={setName}
    placeholder="email@example.com"
/>

UI를 보면 사용자가 입력을 시작하기 전까지 placeholder 텍스트가 표시되는 것을 볼 수 있습니다.

이는 사용자에게 이메일을 입력하라는 안내를 제공합니다.

하지만 placeholder는 레이블을 대체하는 것이 아니라는 점을 기억하세요.

입력을 시작하면 힌트가 사라집니다.

Secure Text Entry

두 번째 prop은 secureTextEntry입니다.

<TextInput
    style={styles.input}
    value={name}
    onChangeText={setName}
    placeholder="Enter your password"
    secureTextEntry={true}
/>

이 prop을 활성화하면 입력 문자가 별표(*) 또는 점(.)으로 표시되어 사용자가 입력한 내용이 감춰집니다.

이는 비밀번호와 같은 민감한 정보를 다룰 때 매우 중요합니다.

Keyboard Type

세 번째로 중요한 prop은 keyboardType입니다.

이 prop은 사용자가 텍스트 입력과 상호작용할 때 나타나는 키보드의 유형을 지정할 수 있게 해줍니다.

예를 들어, 전화번호 필드를 만들 때는 keyboardType을 "numeric"으로 설정할 수 있습니다.

<TextInput
    style={styles.input}
    value={name}
    onChangeText={setName}
    placeholder="Enter your phone number"
    keyboardType="numeric"
/>

Android에서 텍스트 입력 필드를 집중시키면 숫자 키보드가 나타나는 것을 볼 수 있습니다.

이 prop은 사용자 경험(UX)을 향상시킬 수 있습니다.

AutoCorrect

마지막으로 두 가지 props를 더 살펴보겠습니다.

이 props는 기본적으로 활성화되어 있지만, 일부 상황에서는 원하지 않을 수 있습니다.

예를 들어, 잘못된 단어를 입력하고 엔터를 누르면 자동으로 수정되거나, 이름을 입력할 때 자동으로 대문자로 변환될 수 있습니다.

<TextInput
    style={styles.input}
    value={name}
    onChangeText={setName}
    autoCorrect={false}
    autoCapitalize="none"
/>

앱을 다시 시작하고 UI로 돌아가 watre를 입력하고 엔터를 누르면 자동 수정이 되지 않습니다.

또한, John Doe를 입력하고 엔터를 눌러도 자동으로 대문자로 변환되지 않습니다.


Multiline TextInput

이번에는 리액트 네이티브에서 멀티라인 입력을 정의하고 스타일링하는 방법에 대해 알아보겠습니다.

웹에서는 단일 라인 입력을 위한 input 요소와 멀티라인 입력을 위한 textarea 요소가 있지만, 리액트 네이티브에서는 같은 TextInput 요소로 두 가지 용도를 모두 사용할 수 있습니다.

단지 multiLine 속성을 추가해주기만 하면 됩니다.

새로운 TextInput 컴포넌트를 추가해봅시다.

스타일은 Styles.input으로 설정하고, placeholder는 "message"로 지정한 후, multiLine 속성을 추가합니다.

<SafeAreaView style={styles.container}>
    <TextInput
    style={styles.input}
    value={name}
    onChangeText={setName}
    autoCorrect={false}
    autoCapitalize="none"
    />
    <TextInput style={styles.input} placeholder="message" multiline />
    <Text style={styles.text}>My name is {name}</Text>
</SafeAreaView>

이제 UI를 살펴보면, 기본 요소가 같기 때문에 큰 차이를 느낄 수 없습니다.

멀티라인 입력을 눈에 띄게 만들기 위해 추가 스타일을 적용해야 합니다.

Styles 객체에 새로운 key-value 쌍을 추가하여 multiLineText에 최소 높이(minHeight) 100을 설정합니다.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    paddingTop: StatusBar.currentHeight,
  },
  input: {
    height: 40,
    margin: 12,
    padding: 10,
    borderWidth: 1,
  },
  text: {
    fontSize: 30,
    padding: 10,
  },
  multiLineText: {
    minHeight: 100,
  },
});

그 다음, TextInput에 스타일 배열을 지정합니다.

Styles.input과 Styles.multiLineText를 함께 사용합니다.

<TextInput 
    style={[styles.input, styles.multiLineText]}
    placeholder="message"
    multiline
/>

다시 UI를 확인해보면 이제 멀티라인 텍스트 입력처럼 보이는 것을 알 수 있습니다.

사용자는 처음부터 긴 텍스트를 입력할 수 있다는 것을 쉽게 이해할 수 있을겁니다.

중요한 점은, iOS에서는 multiLine 속성이 텍스트 입력을 상단에 정렬하고, 안드로이드에서는 중앙에 정렬한다는 것입니다.

이를 해결하기 위해 textAlignVertical 속성을 "top"으로 설정해야 합니다.

multiLineText: {
    minHeight: 100,
    textAlignVertical: "top",
},

이제 UI로 돌아가서 보면 텍스트가 상단에 제대로 정렬된 것을 확인할 수 있습니다.

이상으로 리액트 네이티브의 TextInput 컴포넌트에 대해 알아보았습니다.


Switch

이번에는 리액트 네이티브의 Switch 컴포넌트에 대해 알아보겠습니다.

Switch 컴포넌트는 앱의 사용자 인터페이스에 토글 기능을 통합하는 데 유용한 도구입니다.

특정 앱 기능을 활성화하거나 비활성화하는 등 사용자가 이진 선택을 해야 하는 상황에 특히 적합합니다.

그럼 VS Code로 돌아가서 사용 방법을 알아봅시다.

우선, react-native 라이브러리에서 Switch 컴포넌트를 임포트합니다.

react-native의 View 컴포넌트를 사용하여 Switch 컴포넌트를 위한 컨테이너를 만듭니다.

스타일은 styles.switchContainer로 설정합니다.

그런 다음, "Dark Mode"라는 텍스트를 표시하는 Text 컴포넌트를 추가하고 스타일을 styles.text로 설정합니다.

그리고 Switch 컴포넌트를 호출합니다.

Switch 컨테이너의 스타일을 정의합니다.

flexDirection을 'row'로 설정하고, alignItems를 'center'로, justifyContent를 'space-between'으로 설정한 후, paddingHorizontal을 10으로 설정합니다.

import {
  StyleSheet,
  Text,
  View,
  StatusBar,
  SafeAreaView,
  TextInput,
  Switch,
} from "react-native";
import { useState } from "react";

export default function App() {
  const [name, setName] = useState("");

  return (
    <SafeAreaView style={styles.container}>
      <TextInput
        style={styles.input}
        value={name}
        onChangeText={setName}
        autoCorrect={false}
        autoCapitalize="none"
      />
      <TextInput
        style={[styles.input, styles.multiLineText]}
        placeholder="message"
        multiline
      />
      <Text style={styles.text}>My name is {name}</Text>
      <View style={styles.switchContainer}>
        <Text style={styles.text}>Dark Mode</Text>
        <Switch />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    paddingTop: StatusBar.currentHeight,
  },
  input: {
    height: 40,
    margin: 12,
    padding: 10,
    borderWidth: 1,
  },
  text: {
    fontSize: 30,
    padding: 10,
  },
  multiLineText: {
    minHeight: 100,
    textAlignVertical: "top",
  },
  switchContainer: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    paddingHorizontal: 10,
  },
});
const styles = StyleSheet.create({
  switchContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingHorizontal: 10,
  },
  text: {
    fontSize: 16,
  },
});

이제 UI를 보면 Switch 컴포넌트가 아래와 같이 보일겁니다.

상태 변수 연결

isDarkMode라는 새로운 상태 변수를 생성하고 초기값을 false로 설정합니다.

그런 다음, Switch 컴포넌트에 두 가지 props를 추가합니다.

valueisDarkMode로 설정하고, onValueChangeisDarkMode 값을 토글하는 함수로 설정합니다.

이 함수는 이전 상태를 받아서 그 값을 반전시킵니다.

export default function App() {
  const [name, setName] = useState("");
  const [isDarkMode, setIsDarkMode] = useState(false);

  return (
    <SafeAreaView style={styles.container}>
      <TextInput
        style={styles.input}
        value={name}
        onChangeText={setName}
        autoCorrect={false}
        autoCapitalize="none"
      />
      <TextInput
        style={[styles.input, styles.multiLineText]}
        placeholder="message"
        multiline
      />
      <Text style={styles.text}>My name is {name}</Text>
      <View style={styles.switchContainer}>
        <Text style={styles.text}>Dark Mode</Text>
        <Switch
          value={isDarkMode}
          onValueChange={() => setIsDarkMode((previousState) => !previousState)}
        />
      </View>
    </SafeAreaView>
  );
}

이제 UI에서 Switch를 토글하여 true와 false 값을 전환할 수 있습니다.

실전 코드에서는 Switch가 켜지거나 꺼질 때 다른 콘텐츠를 표시하거나 특정 기능을 활성화할 수 있습니다.

trackColor와 thumbColor props를 사용하여 트랙과 썸의 색상을 설정할 수 있습니다.

이 색상들은 브랜드 색상에 맞게 조정될 수 있습니다.

<Switch
    value={isDarkMode}
    onValueChange={() => setIsDarkMode((previousState) => !previousState)}
    trackColor={{ false: "#767577", true: "lightblue" }}
    thumbColor="#f4f3f4"
/>

Switch 컴포넌트의 외형은 플랫폼에 따라 다르게 보일 수 있습니다.

이상으로 리액트 네이티브의 Switch 컴포넌트에 대해 알아보았습니다.


Login Form

이번에는 리액트 네이티브에서 폼을 만드는 방법에 대해 알아보겠습니다.

이전에는 입력 필드에 대해 다루었는데요, 이번에는 로그인 폼을 처음부터 끝까지 만들어보겠습니다.

앱 컴포넌트를 최소한의 코드로 리셋했습니다.

View, Text, TextInput, Button, StyleSheet를 임포트하고 시작하겠습니다.

이제 로그인 폼 요소들을 추가해보겠습니다.

1단계: 컨테이너 뷰 추가

먼저, View 컴포넌트를 추가합니다.

이 컴포넌트는 메인 컨테이너 역할을 하며, 깔끔하게 보이도록 컨테이너 스타일을 적용할 것입니다.

import { View, Text, TextInput, Button, StyleSheet } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      {/* 로그인 폼 요소들을 여기에 추가할 것입니다 */}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 20,
    backgroundColor: '#f5f5f5',
  },
});

2단계: 폼 컨테이너 추가

이제 메인 컨테이너 안에 또 다른 View 컴포넌트를 추가합니다.

이 컴포넌트는 폼 컨테이너 역할을 하며, 스타일을 적용하여 폼의 외형을 정의합니다.

export default function App() {
  return (
    <View style={styles.container}>
      <View style={styles.form}>
        {/* 라벨과 입력 필드를 여기에 추가할 것입니다 */}
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 20,
    backgroundColor: '#f5f5f5',
  },
  form: {
    backgroundColor: 'white',
    padding: 20,
    borderRadius: 10,
    shadowColor: 'black',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
});

3단계: 라벨과 입력 필드 추가

이제 폼 컨테이너 안에 사용자 이름과 비밀번호를 위한 라벨과 입력 필드를 추가합니다.

입력 필드에는 안내 텍스트를 위한 placeholder를 설정합니다.

비밀번호 입력 필드는 secureTextEntry를 사용하여 비밀번호를 숨기도록 설정합니다.

export default function App() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  return (
    <View style={styles.container}>
      <View style={styles.form}>
        <Text style={styles.label}>Username</Text>
        <TextInput
          style={styles.input}
          placeholder="Enter your username"
          value={username}
          onChangeText={setUsername}
        />
        <Text style={styles.label}>Password</Text>
        <TextInput
          style={styles.input}
          placeholder="Enter your password"
          secureTextEntry={true}
          value={password}
          onChangeText={setPassword}
        />
        <Button title="Login" onPress={() => {}} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 20,
    backgroundColor: '#f5f5f5',
  },
  form: {
    backgroundColor: 'white',
    padding: 20,
    borderRadius: 10,
    shadowColor: 'black',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
  label: {
    fontSize: 16,
    marginBottom: 5,
    fontWeight: 'bold',
  },
  input: {
    height: 40,
    borderColor: '#ddd',
    borderWidth: 1,
    marginBottom: 15,
    padding: 10,
    borderRadius: 5,
  },
});

4단계: 상태 변수 관리

마지막으로, 상태 변수인 usernamepassword를 생성하고, 입력 필드의 값과 변경 이벤트를 상태 변수와 연결합니다.

import { useState } from 'react';
import { View, Text, TextInput, Button, StyleSheet } from 'react-native';

export default function App() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  return (
    <View style={styles.container}>
      <View style={styles.form}>
        <Text style={styles.label}>Username</Text>
        <TextInput
          style={styles.input}
          placeholder="Enter your username"
          value={username}
          onChangeText={setUsername}
        />
        <Text style={styles.label}>Password</Text>
        <TextInput
          style={styles.input}
          placeholder="Enter your password"
          secureTextEntry={true}
          value={password}
          onChangeText={setPassword}
        />
        <Button title="Login" onPress={() => {}} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 20,
    backgroundColor: '#f5f5f5',
  },
  form: {
    backgroundColor: 'white',
    padding: 20,
    borderRadius: 10,
    shadowColor: 'black',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
  label: {
    fontSize: 16,
    marginBottom: 5,
    fontWeight: 'bold',
  },
  input: {
    height: 40,
    borderColor: '#ddd',
    borderWidth: 1,
    marginBottom: 15,
    padding: 10,
    borderRadius: 5,
  },
});

이렇게 하면 UI에서 로그인 폼을 확인할 수 있습니다.

아래는 안드로이드 화면입니다.

다음에는 폼 검증을 추가하는 방법에 대해 알아보겠습니다.

많은 개발자가 리액트 네이티브에서 입력 필드를 다룰 때 겪는 일반적인 문제를 다뤄 보겠습니다.


KeyboardAvoidingView

자주 발생하는 문제 중 하나는 리액트 네이티브에서 폼을 다룰 때 발생하는 문제입니다.

이번에는 그 문제를 해결하는 방법을 알아보겠습니다.

UI 변경

먼저, UI에 작은 변화를 줘보겠습니다.

사용자 이름 라벨 위에 이미지를 렌더링해보겠습니다.

리액트 네이티브에서 이미지 컴포넌트를 임포트하고, 이미지를 추가합니다.

import { useState } from "react";
import { View, Text, TextInput, Button, Image, StyleSheet } from "react-native";

export default function App() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  return (
    <View style={styles.container}>
      <View style={styles.form}>
        <Image
          source={require("../assets/images/adaptive-icon.png")}
          style={styles.image}
        />
        <Text style={styles.label}>Username</Text>
        <TextInput
          style={styles.input}
          placeholder="Enter your username"
          value={username}
          onChangeText={setUsername}
        />
        <Text style={styles.label}>Password</Text>
        <TextInput
          style={styles.input}
          placeholder="Enter your password"
          secureTextEntry={true}
          value={password}
          onChangeText={setPassword}
        />
        <Button title="Login" onPress={() => {}} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    paddingHorizontal: 20,
    backgroundColor: "#f5f5f5",
  },
  form: {
    backgroundColor: "white",
    padding: 20,
    borderRadius: 10,
    shadowColor: "black",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
  label: {
    fontSize: 16,
    marginBottom: 5,
    fontWeight: "bold",
  },
  input: {
    height: 40,
    borderColor: "#ddd",
    borderWidth: 1,
    marginBottom: 15,
    padding: 10,
    borderRadius: 5,
  },
  image: {
    width: 200,
    height: 200,
    alignSelf: "center",
    marginBottom: 50,
  },
});

이미지가 로그인 폼에 잘 나오고 있습니다.

이제 Command + Shift + K를 눌러 키보드를 눌러봅니다.

키보드 문제가 발생하는 이유

iOS 기기에서 비밀번호 필드를 탭하고 Command + Shift + K를 눌러 키보드를 불러오면 입력 요소가 키보드에 의해 가려지는 문제가 발생합니다.

이는 UX로 보면 상당히 좋지 않습니다.

다행히 리액트 네이티브에는 이를 해결할 수 있는 KeyboardAvoidingView 컴포넌트가 있습니다.

KeyboardAvoidingView 사용

맨 바깥 컨테이너를 KeyboardAvoidingView로 교체하고, behavior 속성을 "padding"으로 설정합니다.

이 속성은 키보드 높이에 맞춰 컴포넌트의 패딩을 증가시킵니다.

import {
  View,
  Text,
  TextInput,
  Button,
  Image,
  StyleSheet,
  KeyboardAvoidingView,
} from "react-native";

export default function App() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  return (
    <KeyboardAvoidingView
      style={styles.container}
      behavior="padding"
    >
      <View style={styles.form}>
        <Image
          source={require("../assets/images/adaptive-icon.png")}
          style={styles.image}
        />
        ...
        ...
      </View>
    </KeyboardAvoidingView>
  );
}

조금은 문제가 해결 된 듯 합니다.

문제 해결

이제 UI로 돌아가서 비밀번호 필드를 탭하면 폼이 위로 이동하여 입력 요소가 가려지지 않는 것을 확인할 수 있습니다.

하지만 이미지 높이를 400으로 설정하면 다시 입력 요소가 가려지는 문제가 발생합니다.

  image: {
    width: 200,
    height: 400, // 400으로 강제로 크게 하면 화면이 가려짐.
    alignSelf: "center",
    marginBottom: 50,
  },

이를 해결하기 위해 keyboardVerticalOffset 속성을 100으로 설정합니다.

<KeyboardAvoidingView
    style={styles.container}
    behavior="padding"
    keyboardVerticalOffset={100}
>
    ...
    ...
</KeyboardAvoidingView>

플랫폼별 해결 방법

Android 기기에서는 불필요한 공간이 생기는 문제가 발생할 수 있습니다.

이를 해결하기 위해 플랫폼별로 다른 offset 값을 적용합니다.

import {
    ...
    ...
    KeyboardAvoidingView,
    Platform
} from "react-native";
~~~
~~~
~~~
<KeyboardAvoidingView
    style={styles.container}
    behavior="padding"
    keyboardVerticalOffset={Platform.OS === 'ios' ? 100 : 0}
>
    ...
    ...
</KeyboardAvoidingView>

이렇게 하면 iOS와 안드로이드 모두에서 UI가 잘 동작하는 것을 확인할 수 있습니다.

KeyboardAvoidingView는 리액트 네이티브에서 까다로운 컴포넌트일 수 있지만, 오늘 다룬 기본 사항을 통해 문제를 해결할 수 있습니다.


Form Validation

이번에는 폼 검증에 대해 알아보겠습니다.

이메일과 비밀번호 필드에 필수 항목 검증을 추가하여 사용자가 올바르게 입력했는지 확인해보겠습니다.

1단계: 에러 메시지 상태 변수 생성

우선, 사용자에게 에러 메시지를 표시할 방법이 필요합니다.

새로운 상태 변수를 생성하여 이러한 메시지를 저장합니다.

이 변수를 errors라고 하고, 초기값은 빈 객체로 설정합니다.

export default function App() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [errors, setErrors] = useState({});

    ...
    ...

}

2단계: 폼 검증 함수 정의

validateForm이라는 함수를 정의합니다.

이 함수는 필드를 검사하고, 에러 메시지를 errors 객체에 추가하거나 모든 필드가 유효한 경우 true를 반환합니다.

export default function App() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [errors, setErrors] = useState({});

  const validateForm = () => {
    let errors = {};

    if (!username) errors.username = "Username is required";
    if (!password) errors.password = "Password is required";

    setErrors(errors);

    return Object.keys(errors).length === 0;
  };

  return (
    <KeyboardAvoidingView
      style={styles.container}
      behavior="padding"
      keyboardVerticalOffset={Platform.OS === "ios" ? 100 : 0}
    >
    ...
    ...
    </KeyboardAvoidingView>
  )
}

3단계: 에러 메시지 표시

이제 JSX에서 에러 메시지를 표시합니다.

TextInput 바로 아래에 에러 메시지를 추가하고, 에러가 있는 경우 해당 메시지를 렌더링합니다.

에러 메시지의 스타일은 styles.errorText로 설정합니다.

<TextInput
    style={styles.input}
    placeholder="Enter your username"
    value={username}
    onChangeText={setUsername}
/>
{errors.username ? (
    <Text style={styles.errorText}>{errors.username}</Text>
) : null}

<TextInput
    style={styles.input}
    placeholder="Enter your password"
    secureTextEntry={true}
    value={password}
    onChangeText={setPassword}
/>
{errors.password ? (
    <Text style={styles.errorText}>{errors.password}</Text>
) : null}

4단계: 에러 텍스트 스타일 정의

에러 텍스트의 스타일을 정의합니다.

텍스트 색상을 빨간색으로 설정하고, 아래쪽에 마진을 추가하여 메시지가 눈에 잘 띄도록 합니다.

errorText: {
    color: "red",
    marginBottom: 10,
},

5단계: 폼 제출 처리

로그인 버튼을 클릭하면 validateForm 함수를 호출하여 폼을 검증합니다. 폼이 유효하면 폼 제출을 처리합니다.

<Button title="Login" onPress={() => { if (validateForm()) { /* Handle form submission */ }}} />

이렇게 하면 간단하면서도 효과적인 폼 검증이 완성됩니다.


Form Submission

1단계: handleSubmit 함수 정의

먼저, 새로운 함수 handleSubmit을 정의합니다.

이 함수 내에서 폼 검증 함수인 validateForm이 true를 반환하는지 확인합니다.

만약 폼이 유효하다면, 폼 값을 콘솔에 로그하고, 제출 메시지를 출력합니다.

또한, 폼 상태와 에러 메시지를 초기화합니다.

export default function App() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});

  const validateForm = () => {
    let errors = {};

    if (!username) errors.username = "Username is required";
    if (!password) errors.password = "Password is required";

    setErrors(errors);

    return Object.keys(errors).length === 0;
  };

  const handleSubmit = () => {
    if (validateForm()) {
      console.log("Submitted", username, password);
      setUsername("");
      setPassword("");
      setErrors({});
    }
  };

...
...

};

2단계: 버튼에 제출 함수 연결

로그인 버튼의 onPress 이벤트에 handleSubmit 함수를 연결합니다.

  return (
    <KeyboardAvoidingView
      style={styles.container}
      behavior="padding"
      keyboardVerticalOffset={Platform.OS === 'ios' ? 100 : 0}
    >
      ...
      ...
        <Button title="Login" onPress={handleSubmit} />
      </View>
    </KeyboardAvoidingView>
  );

3단계: 폼 제출 테스트

이제 UI로 돌아가서 로그인 버튼을 클릭해보세요.

필드를 채우지 않고 클릭하면 에러 메시지가 표시됩니다.

필드를 채운 후 로그인 버튼을 클릭하면 에러 메시지가 사라지고, 폼 상태가 초기화되며, 폼 값이 콘솔에 로그됩니다.

 LOG  Submitted 1111 1111
 LOG  Submitted Test password

마무리

지금까지 리액트 네이티브에서 로그인 폼을 만들고, 상태를 관리하며, 검증을 추가하고, 폼을 제출하는 방법을 배웠습니다.