간단한 로그인 양식 만들기
이메일과 비밀번호 양식을 만들고 입력 길이를 검사하며, 잘못된 입력에 오류 메시지를 표시하고 제출할 때 모의 로그인 함수를 호출합니다.
간단한 로그인 양식 만들기은(는) CoddyKit의 무료 React Native Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 React Native Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Login Form Requirements
A login form is one of the first screens in almost every mobile app. A good implementation must: render email and password fields, validate input (non-empty, valid email format, minimum password length), show field-level error messages below each input, display a loading indicator during the authentication request, show a server error if credentials are wrong, and successfully navigate to the main app on success. This lesson applies TextInput, controlled components, validation logic, and basic state management in one realistic feature.
// State needed for the login form:
// - email: string
// - password: string
// - emailError: string
// - passwordError: string
// - isLoading: boolean
// - serverError: string
// Derived:
// - isSubmitDisabled: !email || !password || isLoadingForm State and Structure
Set up the form's state variables with useState. Using separate states for each field makes updating and validating them independently straightforward. Group the error messages alongside the input values so validation can set and clear them independently. The isLoading flag disables the submit button and shows a spinner while the authentication network request is in progress, preventing double submissions. Clear the serverError whenever the user changes any input value to give them a fresh start.
import { useState } from 'react';
function useLoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [emailError, setEmailError] = useState('');
const [passwordError, setPasswordError] = useState('');
const [serverError, setServerError] = useState('');
const [isLoading, setIsLoading] = useState(false);
function handleEmailChange(text) {
setEmail(text);
if (serverError) setServerError('');
if (emailError) setEmailError('');
}
function handlePasswordChange(text) {
setPassword(text);
if (serverError) setServerError('');
if (passwordError) setPasswordError('');
}
return { email, password, emailError, passwordError, serverError, isLoading, handleEmailChange, handlePasswordChange, setEmailError, setPasswordError, setServerError, setIsLoading };
}Validation Logic
Write a validate() function that checks each field and sets error messages. For email, check that it is non-empty and matches a basic email regex pattern. For password, check that it is at least 8 characters long. The function returns a boolean — true if all fields pass. Only run validation when the user attempts to submit (not on every keystroke, which is annoying). Return early from the submit handler if validation fails, before making any network request. This pattern keeps validation logic separate from the UI and the API call.
function validateForm({ email, password, setEmailError, setPasswordError }) {
let valid = true;
// Email validation
if (!email.trim()) {
setEmailError('Email is required');
valid = false;
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setEmailError('Enter a valid email address');
valid = false;
} else {
setEmailError('');
}
// Password validation
if (!password) {
setPasswordError('Password is required');
valid = false;
} else if (password.length < 8) {
setPasswordError('Password must be at least 8 characters');
valid = false;
} else {
setPasswordError('');
}
return valid;
}The Submit Handler
The submit handler orchestrates validation, the API call, and error handling. It first validates the form and returns early if invalid. Then it sets isLoading to true, makes a mock (or real) authentication request, and handles the response. On success, navigate to the home screen. On failure (wrong credentials), display the server error message. Always reset isLoading in a finally block to ensure the spinner disappears even if the request throws an unexpected error.
async function handleSubmit({
email, password,
setEmailError, setPasswordError,
setServerError, setIsLoading,
onSuccess
}) {
if (!validateForm({ email, password, setEmailError, setPasswordError })) return;
setIsLoading(true);
setServerError('');
try {
// Replace with your real auth API call:
const result = await mockLogin(email, password);
if (result.success) {
onSuccess(result.user);
} else {
setServerError('Invalid email or password. Please try again.');
}
} catch (error) {
setServerError('Network error. Please check your connection.');
} finally {
setIsLoading(false);
}
}
async function mockLogin(email, password) {
await new Promise(r => setTimeout(r, 1200)); // simulate network
if (email === 'test@example.com' && password === 'password123') {
return { success: true, user: { name: 'Test User' } };
}
return { success: false };
}Styled Input Field with Error
Build a reusable FormField component that wraps a styled TextInput with a label above and an error message below. The border color changes to red when there's an error. The FormField accepts all relevant TextInput props plus label and error strings. Using a wrapper component avoids repeating the label/error/border pattern for every input in your form. The error text is conditionally rendered — no empty space appears when there is no error.
import { View, Text, TextInput, StyleSheet } from 'react-native';
export default function FormField({ label, error, style, ...inputProps }) {
return (
<View style={styles.container}>
<Text style={styles.label}>{label}</Text>
<TextInput
style={[
styles.input,
error ? styles.inputError : styles.inputNormal,
style,
]}
placeholderTextColor='#aaa'
{...inputProps}
/>
{error ? <Text style={styles.error}>{error}</Text> : null}
</View>
);
}
const styles = StyleSheet.create({
container: { gap: 4 },
label: { fontSize: 14, fontWeight: '600', color: '#555' },
input: { borderWidth: 1.5, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 13, fontSize: 16, backgroundColor: '#fff' },
inputNormal: { borderColor: '#ddd' },
inputError: { borderColor: '#e74c3c', backgroundColor: '#fff8f8' },
error: { fontSize: 12, color: '#e74c3c' },
});Assembling the Login Screen UI
Put the form together: a logo or app name at the top, the email and password FormField components, the server error banner, the submit button (showing a spinner when loading), and a 'Sign up' link at the bottom. Wrap everything in KeyboardAvoidingView so the keyboard doesn't cover the input fields. Use ScrollView inside so tapping outside dismisses the keyboard. The design follows a clean, vertically centered card layout that looks natural on any phone screen size.
import { KeyboardAvoidingView, ScrollView, View, Text, TouchableOpacity, ActivityIndicator, Platform, StyleSheet } from 'react-native';
import FormField from './FormField';
export default function LoginScreen({ onSuccess, onNavigateToSignUp }) {
const { email, password, emailError, passwordError, serverError, isLoading, handleEmailChange, handlePasswordChange, setEmailError, setPasswordError, setServerError, setIsLoading } = useLoginForm();
function submit() {
handleSubmit({ email, password, setEmailError, setPasswordError, setServerError, setIsLoading, onSuccess });
}
return (
<KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps='handled'>
<Text style={styles.title}>Welcome Back</Text>
<Text style={styles.subtitle}>Sign in to your account</Text>
{serverError ? <Text style={styles.serverError}>{serverError}</Text> : null}
<FormField label='Email' value={email} onChangeText={handleEmailChange} error={emailError} keyboardType='email-address' autoCapitalize='none' autoCorrect={false} returnKeyType='next' />
<FormField label='Password' value={password} onChangeText={handlePasswordChange} error={passwordError} secureTextEntry autoCapitalize='none' returnKeyType='done' onSubmitEditing={submit} />
<TouchableOpacity style={[styles.btn, isLoading && styles.btnLoading]} onPress={submit} disabled={isLoading}>
{isLoading ? <ActivityIndicator color='#fff' /> : <Text style={styles.btnText}>Sign In</Text>}
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
content: { flexGrow: 1, justifyContent: 'center', padding: 24, gap: 16 },
title: { fontSize: 30, fontWeight: '800', color: '#111' },
subtitle: { fontSize: 16, color: '#666', marginBottom: 8 },
serverError: { backgroundColor: '#fff0f0', borderWidth: 1, borderColor: '#fcc', borderRadius: 10, padding: 12, fontSize: 14, color: '#c0392b' },
btn: { backgroundColor: '#4f86f7', padding: 16, borderRadius: 12, alignItems: 'center', marginTop: 8 },
btnLoading: { backgroundColor: '#aac4f7' },
btnText: { color: '#fff', fontWeight: '700', fontSize: 16 },
});Forgot Password Link
Add a 'Forgot Password?' link below the password field. This is a TouchableOpacity with a Text that calls a navigation callback prop. Position it with alignSelf: 'flex-end' so it appears right-aligned below the password field — a common, familiar placement. The link opens a password-reset flow (entering email → receive a reset link), which is typically a separate screen or modal. Keep the login screen focused on the sign-in flow and delegate password reset to its own component.
import { TouchableOpacity, Text, View, StyleSheet } from 'react-native';
export default function ForgotPasswordLink({ onPress }) {
return (
<TouchableOpacity
onPress={onPress}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={styles.link}
accessibilityRole='link'
accessibilityLabel='Forgot password? Tap to reset'
>
<Text style={styles.linkText}>Forgot Password?</Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
link: { alignSelf: 'flex-end' },
linkText: { color: '#4f86f7', fontSize: 14, fontWeight: '500' },
});Sign Up Navigation Link
Most login screens include a 'Don't have an account? Sign up' text link at the bottom. Build this as a row containing plain and linked text. Use nested Text components to mix plain text with the tappable link inline. This is preferable to two separate buttons because it looks cleaner and matches the typographic convention users expect. Wrap the tappable portion in a Text with onPress and link styling, nested inside the outer Text.
import { Text, View, StyleSheet } from 'react-native';
export default function SignUpLink({ onNavigate }) {
return (
<View style={styles.row}>
<Text style={styles.text}>
Don't have an account?{' '}
<Text
style={styles.link}
onPress={onNavigate}
accessibilityRole='link'
>
Sign up
</Text>
</Text>
</View>
);
}
const styles = StyleSheet.create({
row: { alignItems: 'center', marginTop: 8 },
text: { fontSize: 15, color: '#666' },
link: { color: '#4f86f7', fontWeight: '600' },
});Loading and Success States
The login flow has several distinct states that need visual treatment. During the API call (isLoading: true), replace the button text with an ActivityIndicator and disable the button to prevent duplicate requests. On success, navigate away immediately — don't show a success message that the user sees briefly before navigation. On failure, show the server error message prominently and re-enable the button so the user can try again. Shake animation on the error card is a polished touch that draws the eye to the error without requiring the user to scroll.
import { ActivityIndicator, TouchableOpacity, Text, StyleSheet } from 'react-native';
export default function SubmitButton({ isLoading, onPress, disabled }) {
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled || isLoading}
style={[
styles.btn,
(disabled && !isLoading) && styles.btnDisabled,
isLoading && styles.btnLoading,
]}
activeOpacity={0.8}
>
{isLoading
? <ActivityIndicator size='small' color='#fff' />
: <Text style={styles.label}>Sign In</Text>
}
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
btn: { backgroundColor: '#4f86f7', padding: 16, borderRadius: 12, alignItems: 'center', minHeight: 52 },
btnDisabled: { backgroundColor: '#c4d8ff' },
btnLoading: { backgroundColor: '#3a72e0' },
label: { color: '#fff', fontWeight: '700', fontSize: 16 },
});Biometric Authentication Option
Enhance the login screen with a biometric authentication option (Face ID / Touch ID / fingerprint) using expo-local-authentication. Check if the device supports biometrics and if the user has enrolled credentials, then show a biometric login button. On success, retrieve the stored credentials from Keychain/SecureStore and perform the login. Biometric login dramatically improves the experience for returning users who don't want to type their password every session.
import * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
// Install: npx expo install expo-local-authentication expo-secure-store
import { TouchableOpacity, Text } from 'react-native';
import { useEffect, useState } from 'react';
function useBiometrics() {
const [available, setAvailable] = useState(false);
useEffect(() => {
LocalAuthentication.hasHardwareAsync().then(has => {
if (has) LocalAuthentication.isEnrolledAsync().then(setAvailable);
});
}, []);
return available;
}
export default function BiometricButton({ onSuccess }) {
const biometricAvailable = useBiometrics();
if (!biometricAvailable) return null;
async function authenticate() {
const result = await LocalAuthentication.authenticateAsync({ promptMessage: 'Sign in to your account' });
if (result.success) {
const credentials = await SecureStore.getItemAsync('savedCredentials');
if (credentials) onSuccess(JSON.parse(credentials));
}
}
return (
<TouchableOpacity onPress={authenticate} style={{ alignItems: 'center', padding: 16 }}>
<Text style={{ fontSize: 36 }}>👤</Text>
<Text style={{ color: '#4f86f7', fontSize: 14, marginTop: 4 }}>Use Face ID</Text>
</TouchableOpacity>
);
}Social Login Buttons
Many apps offer Sign in with Apple and Sign in with Google as alternatives to email/password login. Apple sign-in is required by Apple's App Store guidelines when you offer any third-party social login. Use expo-apple-authentication for Apple and @react-native-google-signin/google-signin for Google. These buttons must follow platform design guidelines — Apple provides a strict button appearance spec. Place social login buttons above the email/password form with an 'OR' divider to give users a clear choice between methods.
import * as AppleAuthentication from 'expo-apple-authentication';
// npx expo install expo-apple-authentication
import { Platform, View, Text, StyleSheet } from 'react-native';
export default function SocialLogins({ onAppleSuccess, onGoogleSuccess }) {
return (
<View style={styles.container}>
{Platform.OS === 'ios' && (
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={10}
style={styles.appleBtn}
onPress={async () => {
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
onAppleSuccess(credential);
} catch (e) {
if (e.code !== 'ERR_CANCELED') console.error(e);
}
}}
/>
)}
<View style={styles.divider}>
<View style={styles.line} />
<Text style={styles.orText}>OR</Text>
<View style={styles.line} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { gap: 16 },
appleBtn: { width: '100%', height: 48 },
divider: { flexDirection: 'row', alignItems: 'center', gap: 12 },
line: { flex: 1, height: 1, backgroundColor: '#e0e0e0' },
orText: { color: '#999', fontWeight: '500' },
});Quick Check
Test your understanding of React Native Mobile Development concepts from this lesson.
Lesson Recap
In this lesson you learned: validate-on-submit avoids premature error messages during typing, isLoading state disables the submit button and shows a spinner during authentication requests, and FormField wrapper components keep label, input, and error display colocated for reuse across forms. Next up we explore multi-screen navigation with React Navigation.
자주 묻는 질문
“간단한 로그인 양식 만들기” 강의는 무료인가요?
네 — “간단한 로그인 양식 만들기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 React Native Academy 강의 전체를 잠금 해제할 수 있습니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“간단한 로그인 양식 만들기”에서 뭘 배우나요?
이메일과 비밀번호 양식을 만들고 입력 길이를 검사하며, 잘못된 입력에 오류 메시지를 표시하고 제출할 때 모의 로그인 함수를 호출합니다. 브라우저에서 직접 실행하는 실습 코드로 React Native Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
React Native Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 React Native Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“간단한 로그인 양식 만들기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 React Native Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 React Native Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.