07_Contact
코드 블록의 Try it Yourself 버튼으로 직접 실행할 수 있다.
구문
07. Contact.jsx — EmailJS로 이메일 전송
이번 단계에서 할 일
Contact 폼에서 입력한 내용을 실제 이메일로 전송한다. EmailJS — 백엔드 서버 없이 프론트엔드에서 바로 이메일을 보내주는 서비스이다.
피그마 디자인 분석
│ ─── contacts ──────────────────────────────── │
│ │
│ Name │
│ ┌────────────────────────────────────────┐ │
│ │ Value │ │ 높이: 40px
│ └────────────────────────────────────────┘ │
│ │
│ Email │
│ ┌────────────────────────────────────────┐ │
│ │ Value │ │
│ └────────────────────────────────────────┘ │
│ │
│ Message (textarea) │
│ │
│ ┌──────────┐ ┌──────────────────────────┐ │
│ │ Cancel │ │ Submit │ │
│ └──────────┘ └──────────────────────────┘ │
STEP 0 — EmailJS 가입 및 설정
EmailJS를 사용하려면 3가지 값이 필요하다.
| 값 | 설명 | 어디서 찾나 |
|---|---|---|
| Service ID | 연결한 이메일 서비스 식별자 | Email Services 메뉴 |
| Template ID | 이메일 템플릿 식별자 | Email Templates 메뉴 |
| Public Key | 공개 인증 키 | Account > API Keys |
-
EmailJS 가입 — https://www.emailjs.com 에 접속하여 무료 계정을 만든다
-
이메일 서비스 연결 — Email Services 메뉴에서 Gmail 등 사용할 이메일을 연결한다. 연결하면 Service ID가 생긴다 (예:
service_abc123) -
이메일 템플릿 만들기 — Email Templates 메뉴에서 새 템플릿을 만든다. 아래처럼 작성한다
Subject: 이력서 사이트에서 새 메시지가 도착했습니다
Hello,
You got a new message from {{user_name}} ({{user_email}}):
{{message}}
Best wishes,
EmailJS team
{{user_name}}, {{user_email}}, {{message}} — 이중 중괄호 안의 이름은 HTML form의 name 속성과 일치해야 한다. 폼에서 name="user_name"으로 보내면 템플릿에서 {{user_name}}으로 받는다.
-
Template ID 확인 — 템플릿을 저장하면 Template ID가 생긴다 (예:
template_xyz789) -
Public Key 확인 — Account > API Keys 에서 Public Key를 복사한다
STEP 1 — 패키지 설치
npm install @emailjs/browser
@emailjs/browser — 브라우저 환경에서 EmailJS API를 사용할 수 있게 해주는 라이브러리이다
STEP 2 — Contact.jsx 작성
import { useState, useRef } from 'react'
import emailjs from '@emailjs/browser'
import SectionTitle from './SectionTitle'
const Contact = () => {
const formRef = useRef(null)
const [loading, setLoading] = useState(false)
const handleSubmit = (e) => {
e.preventDefault()
setLoading(true)
emailjs
.sendForm(
'YOUR_SERVICE_ID',
'YOUR_TEMPLATE_ID',
formRef.current,
{ publicKey: 'YOUR_PUBLIC_KEY' }
)
.then(() => {
alert('메시지가 전송되었다!')
formRef.current.reset()
})
.catch((error) => {
alert('전송 실패: ' + error.text)
})
.finally(() => {
setLoading(false)
})
}
const handleReset = () => {
formRef.current.reset()
}
return (
<section className="contact" id="contacts">
<SectionTitle
title="contacts"
subtitle="끈기와 열정으로 성장하는 오성의입니다."
/>
<form className="contact-form" ref={formRef} onSubmit={handleSubmit}>
<div className="form-field">
<label className="form-label">Name</label>
<input
className="form-input"
type="text"
name="user_name"
placeholder="이름을 입력하세요"
required
/>
</div>
<div className="form-field">
<label className="form-label">Email</label>
<input
className="form-input"
type="email"
name="user_email"
placeholder="이메일을 입력하세요"
required
/>
</div>
<div className="form-field">
<label className="form-label">Message</label>
<textarea
className="form-textarea"
name="message"
placeholder="메시지를 입력하세요"
required
/>
</div>
<div className="form-buttons">
<button
type="button"
className="form-btn form-btn--cancel"
onClick={handleReset}
>
Cancel
</button>
<button
type="submit"
className="form-btn form-btn--submit"
disabled={loading}
>
{loading ? 'Sending...' : 'Submit'}
</button>
</div>
</form>
</section>
)
}
export default Contact
1: import { useState, useRef } from 'react' — useState는 상태 관리, useRef는 DOM 요소를 직접 참조할 때 사용한다
2: import emailjs from '@emailjs/browser' — EmailJS 라이브러리를 가져온다
3: import SectionTitle from './SectionTitle' — 이전에 만든 섹션 타이틀 컴포넌트이다
6: const formRef = useRef(null) — form 태그를 직접 참조하기 위한 ref이다. EmailJS의 sendForm이 form 요소를 직접 읽어야 하기 때문에 필요하다
7: const [loading, setLoading] = useState(false) — 전송 중인지 아닌지 상태를 저장한다. 전송 중에는 버튼을 비활성화한다
9: const handleSubmit = (e) => { — 폼을 제출할 때 실행되는 함수이다
10: e.preventDefault() — form 태그의 기본 동작(페이지 새로고침)을 막는다
11: setLoading(true) — 전송 시작을 표시한다
13~18: emailjs.sendForm(...) — EmailJS에 이메일 전송을 요청한다
- 첫 번째 인자: Service ID
- 두 번째 인자: Template ID
- 세 번째 인자: form DOM 요소 (
formRef.current) - 네 번째 인자: Public Key 객체
19~21: .then(() => { ... }) — 전송 성공 시 알림을 띄우고, formRef.current.reset()으로 폼을 초기화한다
22~24: .catch((error) => { ... }) — 전송 실패 시 에러 메시지를 보여준다
25~27: .finally(() => { ... }) — 성공/실패 상관없이 loading 상태를 false로 되돌린다
30~32: const handleReset = () => { ... } — Cancel 버튼 클릭 시 폼의 모든 값을 초기화한다
42: ref={formRef} — form 태그에 ref를 연결한다. 이제 formRef.current로 이 form 요소에 접근할 수 있다
47: name="user_name" — EmailJS 템플릿의 {{user_name}}과 매칭되는 이름이다
49: required — 빈 칸으로 제출할 수 없게 막는다
75: disabled={loading} — 전송 중에는 버튼을 클릭할 수 없게 막는다. 중복 전송을 방지한다
77: {loading ? 'Sending...' : 'Submit'} — 삼항 연산자로 전송 중이면 "Sending...", 아니면 "Submit"을 표시한다
'YOUR_SERVICE_ID', 'YOUR_TEMPLATE_ID', 'YOUR_PUBLIC_KEY'는 반드시 본인의 EmailJS 대시보드에서 복사한 실제 값으로 바꿔야 한다.
새로운 개념: useRef
이전 단계에서는 useState로 input 값을 관리했다 (제어 컴포넌트).
이번에는 useRef로 form 자체를 참조한다 (비제어 컴포넌트).
제어 컴포넌트 (useState):
React가 input 값을 직접 관리한다
value={state} + onChange={setState}
비제어 컴포넌트 (useRef):
HTML form이 값을 스스로 관리한다
React는 ref로 form에 접근만 한다
EmailJS의 sendForm은 form DOM 요소를 통째로 읽어서 데이터를 수집한다.
그래서 useRef로 form을 참조하는 방식이 더 자연스럽다.
const formRef = useRef(null)
<form ref={formRef}>
<input name="user_name" />
</form>
emailjs.sendForm('...', '...', formRef.current, { publicKey: '...' })
formRef.current — ref 안에 저장된 실제 DOM 요소이다. useRef는 .current 속성에 값을 담아둔다.
새로운 개념: Promise (.then / .catch / .finally)
emailjs.sendForm()은 Promise를 반환한다.
Promise — 미래에 완료될 작업을 나타내는 객체이다. "나중에 결과를 알려줄게"라는 약속이다.
emailjs.sendForm(...) ← 이메일 전송 요청 (시간이 걸린다)
.then(() => { ... }) ← 성공하면 실행
.catch(() => { ... }) ← 실패하면 실행
.finally(() => { ... }) ← 성공/실패 상관없이 항상 실행
비유: 택배를 보내는 것과 같다.
택배 접수 (sendForm)
├─ 배달 완료 (.then) → "배달되었습니다" 알림
├─ 배달 실패 (.catch) → "배달 실패" 알림
└─ 어떤 경우든 (.finally) → 배달 추적 화면 닫기
STEP 3 — CSS 추가
/* src/index.css 에 추가 */
.contact {
padding: 80px 24px;
}
.contact-form {
width: 320px;
display: flex;
flex-direction: column;
gap: 20px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 16px;
font-weight: 500;
color: #1e1e1e;
}
.form-input {
height: 40px;
padding: 0 12px;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 16px;
color: #1e1e1e;
outline: none;
width: 100%;
}
.form-textarea {
height: 80px;
padding: 10px 12px;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 16px;
color: #1e1e1e;
resize: none;
outline: none;
width: 100%;
}
.form-input::placeholder,
.form-textarea::placeholder {
color: #b3b3b3;
}
.form-input:focus,
.form-textarea:focus {
border-color: #2c2c2c;
}
.form-buttons {
display: flex;
gap: 12px;
}
.form-btn {
height: 40px;
padding: 0 20px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: opacity 0.2s;
}
.form-btn--cancel {
background-color: #f0f0f0;
color: #303030;
}
.form-btn--submit {
flex: 1;
background-color: #2c2c2c;
color: #f5f5f5;
}
.form-btn--submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.form-btn:hover:not(:disabled) {
opacity: 0.8;
}
89~92: .form-btn--submit:disabled — disabled 속성이 있을 때의 스타일이다. 전송 중에는 반투명하게 보이고 커서가 금지 모양으로 바뀐다
94~96: .form-btn:hover:not(:disabled) — 비활성화 상태가 아닐 때만 hover 효과를 적용한다
브라우저 확인
- 폼에 이름, 이메일, 메시지를 입력한다
- Submit 버튼을 클릭한다
- 버튼이 "Sending..."으로 바뀌는 것을 확인한다
- "메시지가 전송되었다!" 알림이 뜨면 성공이다
- EmailJS에 연결한 이메일 받은편지함을 확인한다 — 실제 이메일이 와 있다
전송이 실패하면 확인할 것:
- Service ID, Template ID, Public Key가 정확한지 확인한다
- EmailJS 대시보드에서 이메일 서비스가 Active 상태인지 확인한다
- 템플릿의 변수 이름(
{{user_name}}등)이 form의name속성과 일치하는지 확인한다 - 무료 플랜은 월 200건까지 전송 가능하다
정리
| 개념 | 설명 |
|---|---|
| EmailJS | 백엔드 없이 프론트엔드에서 이메일을 보내는 서비스 |
useRef | DOM 요소를 직접 참조하는 React Hook |
ref={formRef} | form 태그에 ref를 연결하는 방법 |
sendForm() | form 요소를 읽어서 이메일을 전송하는 함수 |
| Promise | 미래에 완료될 작업을 나타내는 객체 |
.then() | 성공 시 실행 |
.catch() | 실패 시 실행 |
.finally() | 성공/실패 상관없이 항상 실행 |
disabled | 버튼을 클릭 불가능하게 만드는 속성 |
required | 빈 칸 제출을 막는 HTML 속성 |