07 Contact
07. Contact.jsx — EmailJS로 이메일 전송
이번 단계에서 할 일
Contact 폼에서 입력한 내용을 실제 이메일로 전송한다. EmailJS — 백엔드 서버 없이 프론트엔드에서 바로 이메일을 보내주는 서비스이다.
피그마 디자인 분석
1│ ─── contacts ──────────────────────────────── │2│ │3│ Name │4│ ┌────────────────────────────────────────┐ │5│ │ Value │ │ 높이: 40px6│ └────────────────────────────────────────┘ │7│ │8│ Email │9│ ┌────────────────────────────────────────┐ │10│ │ Value │ │11│ └────────────────────────────────────────┘ │12│ │13│ Message (textarea) │14│ │15│ ┌──────────┐ ┌──────────────────────────┐ │16│ │ Cancel │ │ Submit │ │17│ └──────────┘ └──────────────────────────┘ │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 메뉴에서 새 템플릿을 만든다. 아래처럼 작성한다
-
Template ID 확인 — 템플릿을 저장하면 Template ID가 생긴다 (예:
template_xyz789) -
Public Key 확인 — Account > API Keys 에서 Public Key를 복사한다
1Subject: 이력서 사이트에서 새 메시지가 도착했습니다2
3Hello,4
5You got a new message from {{user_name}} ({{user_email}}):6
7{{message}}8
9Best wishes,10EmailJS team정보
{{user_name}}, {{user_email}}, {{message}} — 이중 중괄호 안의 이름은 HTML form의 name 속성과 일치해야 한다. 폼에서 name="user_name"으로 보내면 템플릿에서 {{user_name}}으로 받는다.
STEP 1 — 패키지 설치
1npm install @emailjs/browser@emailjs/browser — 브라우저 환경에서 EmailJS API를 사용할 수 있게 해주는 라이브러리이다
STEP 2 — Contact.jsx 작성
1import { useState, useRef } from 'react'2import emailjs from '@emailjs/browser'3import SectionTitle from './SectionTitle'4
5const Contact = () => {6 const formRef = useRef(null)7 const [loading, setLoading] = useState(false)8
9 const handleSubmit = (e) => {10 e.preventDefault()11 setLoading(true)12
13 emailjs14 .sendForm(15 'YOUR_SERVICE_ID',16 'YOUR_TEMPLATE_ID',17 formRef.current,18 { publicKey: 'YOUR_PUBLIC_KEY' }19 )20 .then(() => {21 alert('메시지가 전송되었다!')22 formRef.current.reset()23 })24 .catch((error) => {25 alert('전송 실패: ' + error.text)26 })27 .finally(() => {28 setLoading(false)29 })30 }31
32 const handleReset = () => {33 formRef.current.reset()34 }35
36 return (37 <section className="contact" id="contacts">38 <SectionTitle39 title="contacts"40 subtitle="끈기와 열정으로 성장하는 오성의입니다."41 />42
43 <form className="contact-form" ref={formRef} onSubmit={handleSubmit}>44 <div className="form-field">45 <label className="form-label">Name</label>46 <input47 className="form-input"48 type="text"49 name="user_name"50 placeholder="이름을 입력하세요"51 required52 />53 </div>54
55 <div className="form-field">56 <label className="form-label">Email</label>57 <input58 className="form-input"59 type="email"60 name="user_email"61 placeholder="이메일을 입력하세요"62 required63 />64 </div>65
66 <div className="form-field">67 <label className="form-label">Message</label>68 <textarea69 className="form-textarea"70 name="message"71 placeholder="메시지를 입력하세요"72 required73 />74 </div>75
76 <div className="form-buttons">77 <button78 type="button"79 className="form-btn form-btn--cancel"80 onClick={handleReset}81 >82 Cancel83 </button>84 <button85 type="submit"86 className="form-btn form-btn--submit"87 disabled={loading}88 >89 {loading ? 'Sending...' : 'Submit'}90 </button>91 </div>92 </form>93 </section>94 )95}96
97export default Contact1: 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 자체를 참조한다 (비제어 컴포넌트).
1제어 컴포넌트 (useState):2 React가 input 값을 직접 관리한다3 value={state} + onChange={setState}4
5비제어 컴포넌트 (useRef):6 HTML form이 값을 스스로 관리한다7 React는 ref로 form에 접근만 한다EmailJS의 sendForm은 form DOM 요소를 통째로 읽어서 데이터를 수집한다.
그래서 useRef로 form을 참조하는 방식이 더 자연스럽다.
1const formRef = useRef(null)2
3<form ref={formRef}>4 <input name="user_name" />5</form>6
7emailjs.sendForm('...', '...', formRef.current, { publicKey: '...' })formRef.current — ref 안에 저장된 실제 DOM 요소이다. useRef는 .current 속성에 값을 담아둔다.
새로운 개념: Promise (.then / .catch / .finally)
emailjs.sendForm()은 Promise를 반환한다.
Promise — 미래에 완료될 작업을 나타내는 객체이다. “나중에 결과를 알려줄게”라는 약속이다.
1emailjs.sendForm(...) ← 이메일 전송 요청 (시간이 걸린다)2 .then(() => { ... }) ← 성공하면 실행3 .catch(() => { ... }) ← 실패하면 실행4 .finally(() => { ... }) ← 성공/실패 상관없이 항상 실행비유: 택배를 보내는 것과 같다.
1택배 접수 (sendForm)2 ├─ 배달 완료 (.then) → "배달되었습니다" 알림3 ├─ 배달 실패 (.catch) → "배달 실패" 알림4 └─ 어떤 경우든 (.finally) → 배달 추적 화면 닫기STEP 3 — CSS 추가
1/* src/index.css 에 추가 */2
3.contact {4 padding: 80px 24px;5}6
7.contact-form {8 width: 320px;9 display: flex;10 flex-direction: column;11 gap: 20px;12}13
14.form-field {15 display: flex;16 flex-direction: column;17 gap: 6px;18}19
20.form-label {21 font-size: 16px;22 font-weight: 500;23 color: #1e1e1e;24}25
26.form-input {27 height: 40px;28 padding: 0 12px;29 border: 1px solid #d0d0d0;30 border-radius: 4px;31 font-size: 16px;32 color: #1e1e1e;33 outline: none;34 width: 100%;35}36
37.form-textarea {38 height: 80px;39 padding: 10px 12px;40 border: 1px solid #d0d0d0;41 border-radius: 4px;42 font-size: 16px;43 color: #1e1e1e;44 resize: none;45 outline: none;46 width: 100%;47}48
49.form-input::placeholder,50.form-textarea::placeholder {51 color: #b3b3b3;52}53
54.form-input:focus,55.form-textarea:focus {56 border-color: #2c2c2c;57}58
59.form-buttons {60 display: flex;61 gap: 12px;62}63
64.form-btn {65 height: 40px;66 padding: 0 20px;67 border: none;68 border-radius: 4px;69 font-size: 16px;70 cursor: pointer;71 transition: opacity 0.2s;72}73
74.form-btn--cancel {75 background-color: #f0f0f0;76 color: #303030;77}78
79.form-btn--submit {80 flex: 1;81 background-color: #2c2c2c;82 color: #f5f5f5;83}84
85.form-btn--submit:disabled {86 opacity: 0.5;87 cursor: not-allowed;88}89
90.form-btn:hover:not(:disabled) {91 opacity: 0.8;92}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 속성 |