[๋ฆฌ์•กํŠธ] ๋กœ๊ทธ์ธ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌํ•˜๊ธฐ (API ์—ฐ๋™, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”, ํŽ˜์ด์ง€ ์ด๋™)

2025. 8. 13. 22:26ใ†React

์ตœ์ข… UI

๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ UI

์ด๋ฒคํŠธ ๋™์ž‘ ์ •์˜ํ•˜๊ธฐ 

ํŒŒ๋ž€์ƒ‰: start / end

๋…ธ๋ž€์ƒ‰: ์กฐ๊ฑด 

์ดˆ๋ก์ƒ‰: ์กฐ๊ฑด  ์„ฑ๊ณต ์‹œ ์ˆ˜ํ–‰๋˜๋Š” ์ž‘์—…

๋นจ๊ฐ„์ƒ‰: ์กฐ๊ฑด ์‹คํŒจ ์‹œ ์ˆ˜ํ–‰๋˜๋Š” ์ž‘์—…

 

ํ”Œ๋กœ์šฐ ์ฐจํŠธ

 


 

๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” 

๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ์ž๋™์œผ๋กœ ์•”ํ˜ธํ™”๊ฐ€ ์ง„ํ–‰๋˜๊ณ  ์˜†์˜ ์ฒดํฌ๋ฐ•์Šค ํด๋ฆญ ์‹œ ์ž…๋ ฅํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ณด์ด๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. 

๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ์ž๋™ ์•”ํ˜ธํ™” ํ‘œ์‹œ

 

ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ ์ถœ๋ ฅ

1. ์•„์ด๋”” ๋ฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ฏธ์ž…๋ ฅ

๋ฏธ์ž…๋ ฅ ํ›„ ๋กœ๊ทธ์ธ ์‹œ๋„ ์‹œ ์•„๋ž˜์™€ ๊ฐ™์ด ๋นจ๊ฐ„ ๊ฒฝ๊ณ ๋ฌธ์ด ์ถœ๋ ฅ๋˜๋„๋ก ๊ตฌํ˜„ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. 

์•„์ด๋”” ๋ฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ฏธ์ž…๋ ฅ ํ›„ ๋กœ๊ทธ์ธ ์‹œ๋„

 

 

2. ์กด์žฌํ•˜์ง€ ์•Š์€ ์•„์ด๋””  & ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ 

์กด์žฌํ•˜์ง€ ์•Š์€ ์•„์ด๋””์ด๊ฑฐ๋‚˜ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋กœ๊ทธ์ธ ์‹œ๋„ ์‹œ ์•„๋ž˜์™€ ๊ฐ™์ด ๋นจ๊ฐ„ ๊ฒฝ๊ณ ๋ฌธ์ด ์ถœ๋ ฅ๋˜๋ฉฐ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ๋ž€์— ์žˆ๋˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.   

 

๐Ÿ’ก ๊ฒฝ๊ณ ๋ฌธ์ด “์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜”์ธ ์ด์œ 

์•„์ด๋”” ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ๊ณต๊ฒฉ์ž์—๊ฒŒ ๋…ธ์ถœํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด ์‹คํŒจ ์‹œ ๋ฉ”์‹œ์ง€๋ฅผ ํ†ตํ•ฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

์กด์žฌํ•˜์ง€ ์•Š๋Š” ์•„์ด๋””์ด๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ํ›„ ๋กœ๊ทธ์ธ ์‹œ๋„

 

 

๋กœ๊ทธ์ธ ์„ฑ๊ณต 

๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ์—๋Š” ๋ฉ”์ธํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋ฉฐ ๋ฐฑ์—”๋“œ์—์„œ ๋ฐœ๊ธ‰ํ•ด ์ค€ accessToken์„ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์„ ํƒ ์ด์œ ๋Š” ์•„๋ž˜์— ์„ค๋ช…ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. 

๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ์ด๋™๋œ ๋ฉ”์ธ ํŽ˜์ด์ง€

 

ํšŒ์›๊ฐ€์ž… ๋ฒ„ํŠผ ํด๋ฆญ

ํšŒ์›๊ฐ€์ž… ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

๋ฒ„ํŠผ ํด๋ฆญ ํ›„ ์ด๋™๋œ ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€

 


์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๊ตฌํ˜„ํ•˜๊ธฐ

๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ๊ตฌํ˜„

 ๐Ÿ“—์ฐธ๊ณ ํ•œ ์ž๋ฃŒ

 

[React] ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด๊ธฐ/์ˆจ๊ธฐ๊ธฐ ๊ตฌํ˜„ํ•˜๊ธฐ! - input type ๋ณ€๊ฒฝ

๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด๊ธฐ/์ˆจ๊ธฐ๊ธฐ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์—์„œ ์ด์ œ๋Š” ํ•„์ˆ˜๊ฐ€ ๋˜์–ด๋ฒ„๋ฆฐ ๊ธฐ๋Šฅ์ธ ์ž…๋ ฅํ•˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ณด๊ฑฐ๋‚˜ ์ˆจ๊ธฐ๋Š” ๊ธฐ๋Šฅ์„ ์•„์ฃผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„์„ ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ๋กœ ์ž‘์—…์„ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์•ฝ๊ฐ„

shape-coding.tistory.com

 

๐Ÿ”’๋น„๋ฐ€๋ฒˆํ˜ธ ์ˆจ๊ธฐ๊ธฐ

input ํƒœ๊ทธ์— type์„ password๋กœ ์„ค์ •ํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ˆจ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 

<input 
    type="password"
/>

 

๐Ÿ”“ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ‘œ์‹œํ•˜๊ธฐ

์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•˜๊ณ  ์‹ถ์„ ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด๊ธฐ ์ฒดํฌ ๋ฐ•์Šค๋ฅผ ๋งŒ๋“ค์–ด ์ฒดํฌ ์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”๋ฅผ ํ•ด์ œํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค. 

function SignInField({ baseField, showPassword, setShowPassword, setSignInInfo }) {
    const isPassword = baseField.type === "password";
    const isError = baseField.error !== "";

    return (
        <div className="flex flex-col">
            <div className="flex items-center mb-5">
                ...์ƒ๋žต
                {/* ์ฒดํฌ ๋ฐ•์Šค ํ‘œ์‹œ ๋ถ€๋ถ„ */}
                {
                    isPassword ?
                        <label className="flex items-center mx-5">
                            <input
                                type="checkbox"
                                className="mx-2"
                                checked={showPassword}
                                onChange={(e) => setShowPassword(e.target.checked)}
                            />
                            ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด๊ธฐ
                        </label> : null
                }
            </div>
            ... ์ƒ๋žต
    );
}

 

๐Ÿ”„ ๋™์ž‘ ์›๋ฆฌ

 

 

 

  • ์ฒดํฌ๋ฐ•์Šค ํด๋ฆญ: ์‚ฌ์šฉ์ž๊ฐ€ "๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด๊ธฐ" ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํด๋ฆญ (false → true)
  • ์ด๋ฒคํŠธ ๋ฐœ์ƒ: onChange ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ
  • ์ƒํƒœ ์ฝ๊ธฐ: ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ e.target.checked๋กœ ๋ณ€๊ฒฝ๋œ ์ƒํƒœ(true) ํ™•์ธ
  • ์ƒํƒœ ์—…๋ฐ์ดํŠธ: setShowPassword๋กœ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  • UI ๋ฐ˜์˜: checked={showPassword}๊ฐ€ true๊ฐ€ ๋˜์–ด ์ฒดํฌ๋ฐ•์Šค๊ฐ€ ์ฒดํฌ๋œ ์ƒํƒœ๋กœ ํ‘œ์‹œ
  • ํƒ€์ž… ๋ณ€๊ฒฝ: isPassword && showPassword๊ฐ€ true๊ฐ€ ๋˜์–ด input ํƒ€์ž…์ด "text"๋กœ ๋ณ€๊ฒฝ๋˜๋ฉฐ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‘œ์‹œ

 

function SignInField({ baseField, showPassword, setShowPassword, setSignInInfo }) {
    const isPassword = baseField.type === "password";
    const isError = baseField.error !== "";

    return (
        <div className="flex flex-col">
            <div className="flex items-center mb-5">
                <label className="w-24 text-left mr-4">{baseField.label}</label>
                <input
                    name={baseField.name}
                    {/*type์ด password ์ด๊ณ  showPassword๊ฐ€ true๊ฐ€ ๋˜๋ฉฐ ์•”ํ˜ธํ™”๊ฐ€ ํ•ด์ œ๋ฉ๋‹ˆ๋‹ค.*/}
                    type={isPassword && showPassword ? "text" :baseField.type}
                    value={baseField.value}
                    placeholder={baseField.placeholder}
                    className="pl-5 pr-5 py-2 border border-gray-300 rounded w-80 flex flex-col"
                    onChange={e => setSignInInfo(info => ({
                        ...info,
                        [e.target.name]: e.target.value,
                    }))}
                />
                
                ...์ƒ๋žต

 

ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ ์ถœ๋ ฅ

๐Ÿ“— ์ฐธ๊ณ  ์ž๋ฃŒ

 

ํ”„๋ก ํŠธ์—”๋“œ์˜ ์—๋Ÿฌํ•ธ๋“ค๋ง

์ด๋ฒˆ ๋ฏธ์…˜์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ, ์—๋Ÿฌํ•ธ๋“ค๋ง์„ ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ์ง€ ๊ณ ๋ฏผ์ด ๋งŽ์•˜๋‹ค. ์„œ๋ฒ„์™€ ํ†ต์‹ ๋„ ๊ฐ™์ด ํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์—๋Ÿฌ๊ฐ€ ์ƒ๊ฒจ๋‚ฌ๊ณ , ๊ทธ์ค‘์—๋Š” ๋‚ด๊ฐ€ ํ†ต์ œํ•  ์ˆ˜ ์—†๋Š” ๋ถ€๋ถ„๋„ ์žˆ์—ˆ๋‹ค. ์ด๋ฒˆ ๊ธ€์„ ์ž‘์„ฑ

hae-on.tistory.com

 

 

GitHub - axios/axios: Promise based HTTP client for the browser and node.js

Promise based HTTP client for the browser and node.js - axios/axios

github.com

 

axios post ์ž‘์„ฑ

export function fetchSignIn(signInInfo, setErrors, navigate) {
    axios.post(
        "http://localhost:8080/api/auth/sign-in", //url
        signInInfo,//body
        {
            headers: { "Content-Type": "application/json" } //header
        },
    )
    ...์ƒ๋žต
}

 

์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง (. catch ์‚ฌ์šฉ)

๋กœ๊ทธ์ธ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—๋Ÿฌ๋Š” ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ: 400 Bad Request

์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์›: 404 Not Found

์ธ์ฆ ์‹คํŒจ : 401 Unauthorized

 

fetchSignIn ํ•จ์ˆ˜์—์„œ๋Š” .catch๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„ ์‘๋‹ต ์—๋Ÿฌ๋ฅผ ์žก๊ณ , ์ƒํƒœ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ error ๊ฐ์ฒด์— ๋‹ด์Šต๋‹ˆ๋‹ค.

 

๐Ÿ‘‰๐Ÿป ์ฝ”๋“œ ๋ณด๊ธฐ

๋”๋ณด๊ธฐ
export function fetchSignIn(signInInfo, setErrors, navigate) {
    api.post(
        "/auth/sign-in",
        signInInfo,
    )
        .then((res) => {
            localStorage.setItem("accessToken", res.data.token.trim()),
            setErrors({ loginIdError: "", passwordError: "", globalError: "" }),
            navigate("/");
        })
        .catch(err => {
            const errorMessages = err.response?.data;
            const Errors = {};

            //์ž˜๋ชป๋œ ๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ ๊ฒฝ์šฐ
            if (err.response?.status === 404 || err.response?.status === 401) {
                Errors.globalError = "์•„์ด๋”” ํ˜น์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.";
            }

            //์œ ํšจ์„ฑ ๊ฒ€์ฆ์ด ์‹คํŒจํ•œ ๊ฒฝ์šฐ
            if (err.response?.status === 400) {

                errorMessages.forEach(element => {
                    if (element.includes("์•„์ด๋””")) {
                        Errors.loginIdError = element;
                    }
                    if (element.includes("๋น„๋ฐ€๋ฒˆํ˜ธ")) {
                        Errors.passwordError = element;
                    }
                });
            }

            setErrors(prev => ({ ...prev, ...Errors }));
        })
}

 

์ฝ”๋“œ ์„ค๋ช…:

  • .catch์—์„œ ์„œ๋ฒ„ ์‘๋‹ต(err.response)์„ ํ™•์ธํ•˜๊ณ  ์ƒํƒœ ์ฝ”๋“œ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฉ”์‹œ์ง€๋ฅผ Errors ๊ฐ์ฒด์— ๋‹ด์Šต๋‹ˆ๋‹ค.
  • 404, 401 ์—๋Ÿฌ๋Š” ํ•˜๋‚˜์˜ ๊ณตํ†ต ๋ฉ”์‹œ์ง€๋ฅผ globalError๋กœ ์„ค์ •ํ•˜๊ณ , 400 ์—๋Ÿฌ๋Š” ํ•„๋“œ๋ณ„ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ๋ถ„ํ•˜์—ฌ loginIdError, passwordError์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ๋งˆ์ง€๋ง‰์— setErrors๋กœ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ํ™”๋ฉด์— ํ‘œ์‹œํ•  ์ค€๋น„๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.

 

์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ปดํฌ๋„ŒํŠธ

๊ฐ ํ•„๋“œ์™€ ๊ธ€๋กœ๋ฒŒ ์—๋Ÿฌ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

 

 

  • FieldErrorMessage: ์•„์ด๋””, ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„๋“œ ์˜†์— ํ‘œ์‹œ
  • GlobalErrorMessage: ๋กœ๊ทธ์ธ ์ „์ฒด ์‹คํŒจ ์‹œ ์ค‘์•™์— ํ‘œ์‹œ

 

function FieldErrorMessage({ value }) {
    return (
        <p
            className="font-bold text-top text-xs text-red-400 ml-28 mb-10"
        > {value}</p>
    );
}

function GlobalErrorMessage({ value }) {

    return (
        <p
            className="flex font-bold text-xs text-red-400 justify-center mt-10"
        > {value}</p>
    );
}

 

 

์‘๋‹ต ํ† ํฐ ์ €์žฅํ•˜๊ธฐ

๐Ÿ“— ์ฐธ๊ณ ์ž๋ฃŒ

 

[React] JWT ํ† ํฐ ์ €์žฅ ์œ„์น˜

์„œ๋ฒ„์™€ API ์—ฐ๋™์„ ํ•  ๋•Œ JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์•„ ์ธ์ฆ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.์—ฐ๋™ ์ค‘ ํ† ํฐ์„ ์–ด๋””์— ์ €์žฅํ•ด์•ผ ํ• ๊นŒ? ์— ๋Œ€ํ•œ ์˜๋ฌธ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.ํ† ํฐ์„ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์žˆ์ง€๋งŒ ๋Œ€ํ‘œ์ ์œผ๋กœ ๋‘๊ฐ€

velog.io

 

 

[Spring Boot + React] ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ๊ณผ ์•ก์„ธ์Šค ํ† ํฐ ์ €์žฅ ๋ฐฉ๋ฒ• ๋ณ€๊ฒฝ (HTTPOnly ์ฟ ํ‚ค, ์ „์—ญ ์ƒํƒœ๊ด€๋ฆฌ)

์ด ๊ธ€์—์„œ ์‚ฌ์šฉํ•œ ๊ธฐ์ˆ  ์Šคํƒ ๋ฒ„์ „ ๋”๋ณด๊ธฐ ์„œ๋ฒ„ Spring boot 3.0.2 Spring Security 6.0.1 JJWT 0.9.1 ํด๋ผ์ด์–ธํŠธ React 18.2.0 axios 1.6.7 ๊ธฐ์กด์—๋Š” ๋กœ๊ทธ์ธ ํ›„ ๋ฐœ๊ธ‰๋ฐ›์€ ์•ก์„ธ์Šค ํ† ํฐ์„ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—, ๋ฆฌํ”„๋ ˆ์‹œ ํ† 

cr0c0.tistory.com

 

ํ† ํฐ(JWT)์„ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•

  • ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ 
  • ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€
  • ์ฟ ํ‚ค

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํ˜„์žฌ ์ƒํ™ฉ์— ๋งž์ถฐ ๋ถˆํ•„์š”ํ•œ ๋ณต์žก์„ฑ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. 

 

[์ด์œ  1]

ํ”„๋ก ํŠธ์—”๋“œ ํ•™์Šต์šฉ ํ”„๋กœ์ ํŠธ๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•  ํ† ํฐ์„ accessToken ํ•˜๋‚˜๋กœ ์„ค๊ณ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

โžก๏ธ ๋ณต์žกํ•œ ์ธ์ฆ ํ๋ฆ„์ด๋‚˜ Refresh Token ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.


[์ด์œ  2]

ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์งง๊ฒŒ ์„ค์ •ํ•  ์˜ˆ์ •์ด๊ธฐ ๋•Œ๋ฌธ์— ํƒˆ์ทจ๋‹นํ•˜๋”๋ผ๋„ ์œ„ํ—˜์ด ์ ์„ ๊ฒƒ์ด๋ผ๋Š” ํŒ๋‹จ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

โžก๏ธ ์‚ฌ์šฉ์ž์˜ ๊ฒฝํ—˜์ด ๋‚ฎ์•„์ง€๋ฉฐ, ๋งŒ์•ฝ ๊ณต๊ฒฉ์ž๊ฐ€ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ๋Š˜๋ฆฐ๋‹ค๋ฉด ๋ฐฑ์—”๋“œ๊ฐ€ ์ด๋ฅผ ํ™•์ธํ•  ๋ฐฉ๋ฒ•์ด ์—†์–ด ์—ฌ์ „ํžˆ ์œ„ํ—˜์ด ์กด์žฌํ•œ๋‹ค๋Š” ์ ์€ ์ธ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. 

 

๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์˜ ๊ฐ’์€ ์ฝ˜์†”์„ ํ†ตํ•ด์„œ ๋ฐ”๋กœ ํ™•์ธ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

์ฝ˜์†”์„ ํ†ตํ•ด ํ™•์ธํ•œ ํ† ํฐ ๊ฐ’

 

์ถ”ํ›„ ํ™•์žฅ ๊ฐ€๋Šฅ์„ฑ

ํ–ฅํ›„ ์‹ค์ œ ์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด, ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ์•ˆ์„ ๊ณ ๋ คํ•  ๊ณ„ํš์ž…๋‹ˆ๋‹ค.

  • ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์™€ ์ฟ ํ‚ค๋ฅผ ์ด์šฉํ•œ accessToken๊ณผ Refresh ํ† ํฐ ์ €์žฅ 
  • Redis์— Refresh Token ์ €์žฅ์œผ๋กœ ์•ฝ๊ฐ„์˜ stateful ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ด ํ† ํฐ ํƒˆ์ทจ ๋ฐ ์กฐ์ž‘ ์œ„ํ—˜ ์™„ํ™”

 

๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์‚ฌ์šฉํ•˜๊ธฐ (์ €์žฅ ๋ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ)

๐Ÿ“— ์ฐธ๊ณ  ์ž๋ฃŒ

 

LocalStorage๋กœ ์ €์žฅ, ๋ถˆ๋Ÿฌ์˜ค๊ธฐ, ์‚ญ์ œ (JS, React)

ํŠน๋ณ„ํžˆ ์„œ๋ฒ„๋‹จ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ์˜ˆ์ •์ด๊ณ , ์ค‘์š”ํ•œ ๋‚ด์šฉ์„ ๋‹ด๊ณ  ์žˆ์ง€ ์•Š๋‹ค๋ฉด ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜์ด๋‹ค. ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์–ด๋–ป๊ฒŒ ์ด์šฉํ•˜์—ฌ ์ €์žฅ, ๋ถˆ๋Ÿฌ์˜ค๊ธฐ, ์‚ญ์ œ๋ฅผ ํ• 

patrick-f.tistory.com

 

๋กœ๊ทธ์ธ ์‹œ localStorage.setItem( "์ €์žฅํ•˜๊ณ  ์‹ถ์€ ์ด๋ฆ„", ๋„ฃ๊ณ  ์‹ถ์€ ๋ฐ์ดํ„ฐ)์„ ํ†ตํ•ด ํ† ํฐ ์ €์žฅ

import axios from "axios";

export function fetchSignIn(signInInfo) {
    axios.post(
        "http://localhost:8080/api/auth/sign-in",
        signInInfo,
        {
            headers: { "Content-Type": "application/json" }
        },
    )
        .then((res) =>
            localStorage.setItem("accessToken", res.data.token.trim())
        )
        .catch(err => console.log(err));
}

 

๐Ÿ‘‰๐Ÿป trim์„ ์‚ฌ์šฉํ•œ ์ด์œ  

๋”๋ณด๊ธฐ

์‚ฌ์šฉ ์ „ ๋ฐฑ์—”๋“œ์—์„œ ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค. 

JWT strings may not contain whitespace ์—๋Ÿฌ ๋ฉ”์„ธ์ง€

 

์ฝ˜์†”๋กœ ํ† ํฐ ๊ฐ’์„ ํ™•์ธํ•ด ๋ณด๋‹ˆ, ๋ฐฑ์—”๋“œ์™€ ํ”„๋ก ํŠธ ๋ชจ๋‘์—์„œ ํ† ํฐ์ด ์—ฌ๋Ÿฌ ์ค„๋กœ ์ถœ๋ ฅ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • ์•„๋งˆ ์ „์†ก ๊ณผ์ •์—์„œ ํ† ํฐ์ด ๋„ˆ๋ฌด ๊ธธ์–ด ์ค„๋ฐ”๊ฟˆ์ด ๋ฐœ์ƒํ•œ ๊ฒƒ์œผ๋กœ ์ถ”์ธก๋ฉ๋‹ˆ๋‹ค.
  • ์ด๋กœ ์ธํ•ด JWT๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฌธ์ž์—ด๋กœ ์ธ์‹๋˜์–ด ์œ„์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. 

 

Intellij ์ฝ˜์†”์—์„œ ์ฐ์€ Token ๊ฐ’

 

๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์ฐ์€ Token ๊ฐ’

 

๋‹ค๋ฅธ api์—์„œ๋Š” localStorage.getItem( "์ €์žฅํ–ˆ๋˜ ์ด๋ฆ„")์„ ํ†ตํ•ด accessToken์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 

 

ํŽ˜์ด์ง€ ์ด๋™ํ•˜๊ธฐ ( useNavigate)

๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ(๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ): ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ 

ํšŒ์›๊ฐ€์ž… ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ : ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€ ์ด๋™

 

๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ์—๋Ÿฌ ์—†์ด ์ •์ƒ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋‹ค๋ฉด ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. 

 

๐Ÿ’กํ›…์˜ ์œ„์น˜

useNavigate์™€ ๊ฐ™์€ ํ›…์€ React๊ฐ€ ์ƒํƒœ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ปดํฌ๋„ŒํŠธ ์ตœ์ƒ์œ„์— ์œ„์น˜์‹œ์ผœ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
๋”ฐ๋ผ์„œ, ์ƒ๋‹จ์—์„œ useNavigate๋ฅผ ์„ ์–ธํ•œ ๋’ค, ํ•ด๋‹น ๊ฐ’์„ fetch ํ•จ์ˆ˜์— props๋กœ ์ „๋‹ฌํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

โžก๏ธ React๋Š” ํ›…์˜ ํ˜ธ์ถœ ์ˆœ์„œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ƒํƒœ๋ฅผ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. 

 

SignIn ์ปดํฌ๋„ŒํŠธ

function SignInPage() {
    const [showPassword, setShowPassword] = useState(false);
    const [signInInfo, setSignInInfo] = useState({ loginId: "", password: "" });
    const [errors, setErrors] = useState({ loginIdError: "", passwordError: "", globalError: "" });
    const navigate = useNavigate() // navigate ํ• ๋‹น

    const fields = [
        { name: "loginId", label: "์•„์ด๋””", type: "text", value: signInInfo.loginId, placeholder: "์•„์ด๋””๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", error: errors.loginIdError },
        { name: "password", label: "๋น„๋ฐ€๋ฒˆํ˜ธ", type: "password",value: signInInfo.password, placeholder: "๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", error: errors.passwordError }
    ];

    //๋กœ๊ทธ์ธ ํผ ์ œ์ถœ 
    function handleSignInSubmit(e, signInInfo, setErrors, navigate) {
        e.preventDefault();

        setErrors({ loginIdError: "", passwordError: "" });
        fetchSignIn(signInInfo, setErrors, navigate) //api ํ˜ธ์ถœ ํ•จ์ˆ˜ 
    }

 

fetchSignIn ํ•จ์ˆ˜

export function fetchSignIn(signInInfo, setErrors, navigate) {
    api.post(
        "/auth/sign-in",
        signInInfo,
    )
        .then((res) => {
            localStorage.setItem("accessToken", res.data.token.trim()),
            setErrors({ loginIdError: "", passwordError: "", globalError: "" }),
            navigate("/"); //์ด๋™
})

 

 

์ด๋ฒˆ ๊ณผ์ •์—์„œ ๊ฐ€์žฅ ๊ณ ๋ฏผํ–ˆ๋˜ ๋ถ€๋ถ„์€ ํ† ํฐ ์ €์žฅ ๋ฐฉ๋ฒ•๊ณผ ๋ฐฑ์—”๋“œ์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ์˜€๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. 
์•„์ง๋„ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ถ€๋ถ„์€ ์™„์ „ํžˆ ๊ฐ์ด ์žกํžˆ์ง€ ์•Š์•˜๊ณ , ๋งŒ์•ฝ ํ˜„์žฌ ํฌ์Šคํ„ฐ์—์„œ ๋‹ค๋ฃฌ ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋‹ค์–‘ํ•œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด, ์ง€๊ธˆ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์€ ํ•˜๋“œ ์ฝ”๋”ฉ์ด ๊ณ„์† ๋Š˜์–ด๋‚˜๋Š” ๋“ฑ ํ•œ๊ณ„๊ฐ€ ์กด์žฌํ•  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. 

 

๋‹ค์Œ ํฌ์Šคํ„ฐ์—์„œ๋Š” ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ค๋ฃจ๋ฉด์„œ, ๋‹ค์‹œ ํ•œ๋ฒˆ ์—๋Ÿฌ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.