비밀번호 보기/숨기기 (useToggle 커스텀 훅)

튜토리얼 — 비밀번호 보기/숨기기 (useToggle 커스텀 훅)
useLocalStorage는 상태 + 동기화 로직을 한 묶음으로 숨겼습니다. 커스텀 훅이 잘하는 건 그게 다가 아니에요. 훨씬 단순한 묶음 도 훅으로 빼면 깔끔합니다 — "불리언 한 개와 그걸 뒤집는 함수"가 그 자리에 자주 등장합니다.
이번엔 그 묶음을 useToggle이라는 훅으로 만들고, 비밀번호 보기/숨기기 토글에 써 봅니다. useLocalStorage와 모양이 똑같아서 패턴 복습에 딱 맞아요.
개념
useToggle 한 줄을 부르면 다음이 한꺼번에 옵니다.
- 현재 켜짐/꺼짐 값
- 그걸 뒤집는 함수
const [visible, toggleVisible] = useToggle(false);
useState처럼 두 칸짜리 배열로 받습니다 — useLocalStorage에서 본 그 모양이에요. 다른 점은 함수가 인자를 안 받는다는 것뿐입니다 (그냥 뒤집기만 하니까).
이번 훅 안에는 useEffect는 안 들어갑니다. 바깥 세상(localStorage·이벤트 등)과 동기화할 일이 없거든요. 커스텀 훅이 꼭 useEffect를 품어야 하는 건 아닙니다 — useState 하나만 묶어도 충분히 가치가 있어요. 핵심은 "같은 모양의 묶음이 여러 곳에서 나타나면 훅으로 빼낸다"는 것 자체입니다.
함께 해보기
1단계 — useToggle 훅 만들기
hooks/useToggle.ts 파일을 만듭니다.
// hooks/useToggle.ts import { useState } from "react"; export function useToggle( initialValue: boolean = false, ): [boolean, () => void] { const [value, setValue] = useState(initialValue); function toggle() { setValue((current) => !current); } return [value, toggle]; }
useLocalStorage와 한 줄씩 비교하며 보세요.
initialValue: boolean = false— 안 넘기면 기본false.useState(false)처럼 시작값을 그냥 줘도 되고요(useToggle(true)).: [boolean, () => void]— 반환 타입을 명시. "불리언 하나와, 인자 없는 함수 하나로 이루어진 배열"이라는 뜻입니다.useLocalStorage에서[string, (value: string) => void]라고 적었던 것과 똑같은 자리예요. 이 타입을 안 적으면 호출 쪽에서[boolean, () => void]가 아니라 더 느슨한 추론이 잡혀 다루기 불편해집니다.setValue((current) => !current)—!value가 아니라 함수형 업데이트를 씁니다. 같은 렌더에서 토글을 두 번 빠르게 부르더라도 두 번째 호출이 첫 번째 결과를 안 보고 옛value를 뒤집어 버리는 문제를 피하려고요. 토글은 안전하게 이렇게 쓰는 게 습관이 됩니다.
2단계 — PasswordInput 컴포넌트
components/PasswordInput.tsx 파일을 만듭니다.
// components/PasswordInput.tsx "use client"; import { useState } from "react"; import { useToggle } from "@/hooks/useToggle"; export default function PasswordInput() { const [password, setPassword] = useState(""); const [visible, toggleVisible] = useToggle(false); return ( <div className="p-4 space-y-2"> <label className="block">비밀번호</label> <div className="flex gap-2"> <input type={visible ? "text" : "password"} className="flex-1 border px-2 py-1" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="비밀번호 입력" /> <button type="button" className="rounded bg-gray-200 px-3 py-1" onClick={toggleVisible} > {visible ? "🙈 숨기기" : "👁 보기"} </button> </div> <p className="text-sm text-gray-500"> 현재 상태: {visible ? "보임" : "숨김"} </p> </div> ); }
useToggle 한 줄로 visible 과 toggleVisible이 같이 나옵니다 — useState 한 줄을 부른 것 같죠. 비교해 보면 차이가 분명합니다.
// 훅 없이 — useState + toggle 함수를 매번 손으로 const [visible, setVisible] = useState(false); function toggleVisible() { setVisible((v) => !v); } // 훅으로 — 한 줄 const [visible, toggleVisible] = useToggle(false);
뒤집기가 두세 군데 나오는 화면이라면 (사이드바, 모달, 비밀번호…) 매번 손으로 짜는 게 금세 답답해집니다. 한 번 훅으로 빼두면 그 다음부터는 한 줄.
3단계 — 페이지에 올리기
app/password/page.tsx를 만듭니다.
import PasswordInput from "@/components/PasswordInput"; export default function PasswordPage() { return ( <main style={{ padding: 24 }}> <h2>비밀번호 보기 / 숨기기</h2> <PasswordInput /> </main> ); }
4단계 — 확인
/password를 엽니다.
- 초기엔 입력칸이
password타입 — 글자를 쳐도•로 가려집니다. 옆 버튼은 "👁 보기". ✅ - "👁 보기" 클릭 → 입력칸이
text로 바뀌어 글자가 그대로 보입니다. 버튼은 "🙈 숨기기"로 바뀜. ✅ - 다시 클릭 → 원래대로 숨김. ✅
- 빠르게 여러 번 눌러도 켜짐/꺼짐이 정확히 번갈아 갑니다 (함수형 업데이트의 효과예요).
확인 ✅ — PasswordInput 안에는 "토글"이라는 단어와 관련된 로직이 한 글자도 없습니다. 전부 useToggle 한 줄로 끝났어요. 8장의 PersistNote가 useLocalStorage로 줄어든 것과 정확히 같은 변신입니다.
정리
- 커스텀 훅은 useEffect를 꼭 품을 필요는 없습니다.
useState하나만 묶어도 가치가 있어요. - 반환을
[값, 함수]모양으로 — 반환 타입(: [boolean, () => void])을 명시하면 호출 쪽에서useState처럼 자연스럽게 쓸 수 있습니다. - 토글은 함수형 업데이트 (
(v) => !v)로 — 빠른 연속 호출에서도 안전. - "같은 묶음이 여러 곳에 나타나면 훅으로 뺀다" — 그 묶음이 작아도 됩니다.
연습 거리
- 사이드바 토글 —
useToggle을 사이드바 메뉴(isOpen) 에도 그대로 적용. 한 줄로 추가됩니다. - 명시적 on/off 추가 — 반환을
[value, { toggle, on, off }]로 확장.on은 "무조건 켜기",off는 "무조건 끄기". 어떤 화면에서는 토글보다 명시적 켜기/끄기가 더 자연스러워요.const [open, { toggle, on, off }] = useToggle(false); useCounter(initial)만들기 — 같은 식으로[count, { increment, decrement, reset }]을 돌려주는 훅. 카운터가 여러 곳에 필요할 때 유용합니다.

댓글
댓글을 작성하려면 이 필요합니다.