Futsal Manager - 02

윤수빈·어제
0

1. 서론

Futsal Manager - 01 에 이어서 진행한 내용을 정리하며 웹 브라우저에서 서버와 통신하고 API를 호출하여 브라우저에 보여주는(렌더링) 방법에 대해 공부하고 그 과정에서 있었던 문제들에 대해 다루어 보려고 한다.

서버 작업도 바쁜데 클라이언트 작업은 왜 하는가?

우리 프로젝트 기획에 와이어프레임(브라우저)를 설계 및 구현하고 API 응답 결과를 브라우저를 통해 나타내기 위해서이다...

2. 본론

2-1. 로그인

우선 로그인을 먼저 거쳐야 futsal manager 홈으로 이동할 수 있다.
(여기서 토큰이 없다면 강제로 로그인 화면으로 이동시켜주고 싶지만 방법을 알아봐야한다)

-- index.html --
    <body>
        <div class="title">
            <h1>차고차고</h1>
        </div>
        <div class="login-container">
            <h2>로그인</h2>
            <form action="/api/account/login" method="post" id="login-form">
                <label for="id">아이디</label>
                <input type="id" id="userId" name="userId" placeholder="아이디를 입력하세요."/>
                <label for="password">비밀번호</label>
                <input type="password" id="password" name="password" placeholder="비밀번호를 입력하세요." />
                <div class="login_button">
                    <button type="regist" id="registBtn" name="account_regist">계정 생성</button>
                    <button type="submit" id="confirmBtn" name="login_confirm">확인</button>
                </div>
            </form>
        </div>
    </body>

로그인 화면은 위와 같이 구현한 상태이다.

login-form 타입으로 지정해주고 확인을 누르면 입력한 userId, password 값을 기준으로 로그인 API를 호출할 수 있도록 하려고 했다.

2-1-1. 정적 파일 제공 (html)

주소창에 입력을 했을 때 위에 있는 로그인 화면(index.html)을 어떻게 띄울 수 있을까?

이전에 todo-list 할일 목록 게시판 API 구현 강의내용을 진행했을 때 express.static()으로 정적파일을 불러오는 것이 기억이 번뜩 나서 해보려고 했다.

하지만 어떻게 해도 실패... 404 에러로 찾을 수 없다고 떴다..

🔎 문제 접근

express.static() 은 Express에서 기본으로 제공하는 미들웨어 함수로 이미지, CSS, JS 파일과 같은 정적파일을 제공하기 위해서 사용한다.

때문에 정적 리소스가 있는 디렉토리 이름을 express.static 인자로 전달해주면 파일을 제공해주는 형태가 되는 것이다.

예를 들어, app.use(express.static('browser')) 를 해주면

http://localhost:port/images/~~.png
http://localhost:port/css/~~.css
http://localhost:port/js/~~.js
http://localhost:port/index.html

처럼 주소를 입력하여 정적 파일을 불러올 수 있는 것이다.

또한, Express는
정적 디렉토리에 대해 상대적으로 파일을 검색한다. 정적 디렉토리의 이름이 URL의 일부가 아니다.

그리고, 이러한 디렉토리 주소는 node 프로세스가 실행되는 디렉토리에 대해 상대적이기 때문에 기본 경로를 지정해주는 것이 다른 환경에서도 편하게 실행할 수 있다.

코드 해석 후 적용

  1. 경로를 찾지 못했던 이슈가 있어서 기본 경로를 지정해주었다. 기본으로 제공되는 pathfileURLToPath를 활용하여 __dirname이라는 변수에 지정해 준다.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
  1. 상대경로와 합쳐 browser 라는 폴더에 있는 파일을 불러올 수 있도록 지정해 주었다.
    app.use(express.static(path.join(__dirname, 'browser')));

  2. localhost:PORT/api에 접근하면 browser/index.html에 있는 파일을 제공할 수 있도록 설정해주었다.

// 로그인 메인홈
app.get('/api', (req, res) => {
    res.sendFile(path.join(__dirname, 'browser/index.html'));
});

결과

이제 파일이 정상적으로 제공된다!! 야호!

2-1-2. 버튼 클릭으로 API 호출

이전에 배운 Fetch를 통해 API를 호출하려고 했는데 어떻게 페이지를 이동시킬 수 있을지 고민이 되었다.

📁 폴더 구분

일단 클라이언트에서 작동하는 것이기 때문에 Server 폴더와 분리를 해주었다.

--- Main Folder ---
/src/browser 			=> 클라이언트 API 작동 정의
/src/routes 			=> 서버 API 작동 정의

browser 폴더 내부 설명은 아래와 같다.

/browser/html			=> html 파일이 모여있음
/browser/scripts		=> html 에서 작동하는 기능(js)가 모여있음
/borwser/styles			=> html 스타일이 모여있음

scripts 폴더 내부에서는 각 기능별 모듈화를 진행하였다.

/scripts/api.js			=> Fetch를 통해 API를 호출하는 함수들에 대해 정의
/scripts/~~UI.js		=> 웹 UI에서 Event 발생을 통해 작동하는 함수들에 대해 정의
/scripts/util.js		=> 공통적으로 수행되는 함수들에 대해 정의
/scripts/game.js		=> 게임 진행 시 작동하는 로직에 대해 정의

📑 코드 실행 순서

  1. 라우터에서 반환할 내용을 정하기
--- account.router.js ---
/** 사용자 로그인 API **/
router.post('/account/login', async (req, res, next) => {
    try {
        const { userId, password } = req.body;
        const user = await prisma.account.findFirst({ where: { userId } });

        if (!user) 
        	throw new Error('AccountNotFound');
        else if (!(await bcrypt.compare(password, user.password))) 
			throw new Error('InvalidPassword');

        const accessToken = createAccessToken(user.accountId);

        res.header('authorization', `Bearer ${accessToken}`);

        return res.status(200).json({ isLogin: true, token: accessToken });
    } catch (err) {
        next(err);
    }
});

위 account 라우터 미들웨어에서는 로그인 API가 호출되면 res.status와 res.json()을 반환해주는데 json()에는 로그인이 되었다는 isLogin과 사용자 인증을 위한 token을 전달해주게 된다.

  1. Fetch를 통해 호출한 API에서 반환된 내용으로 서버 응답에 따라 1차적으로 상태를 처리
--- api.js ---
/** 계정 로그인 API 호출 */
export async function loginAccount(body) {
    return fetch('/api/account/login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
    }).then(res => {
        // 정상적인 응답 처리
        if (res.status === 200) {
            return res.json();
        }
        // 기타 상태 코드에 대한 처리
        else return false
    });
}

위 코드에서는 userId, password를 받아서 body 데이터로 전달하도록 했다.
fetch 수행을 한 뒤 then()을 통해 응답에 따른 반환 내용을 정리해주었다.

  1. 브라우저에서 API 반환에 따른 렌더링 작업
--- indexUI.js ---
/** 로그인 버튼 기능 함수 */
document.getElementById('login-form').addEventListener('submit', function (event) {
    event.preventDefault();

    const userId = document.getElementById('userId').value;
    const password = document.getElementById('password').value;

    const body = { userId, password };

    loginAccount(body).then(res => {
        if (!res) {
            alert('아이디 혹은 비밀번호가 일치하지 않습니다.');
            return;
        } else {
            window.location.href = 'http://localhost:3333/api/category';
        }
    });
});

위 코드는 index.html 에서 [확인] 버튼에 대한 이벤트 리스너를 받아서 'submit'이 되면 2번의 api.js에서 정의한 loginAccount() 함수가 호출되도록 한 것이다.

loginAccount()에서 반환된 내용에 따라 .then(res) 를 통해 결과에 대한 내용을 클라이언트에서 알려줄 수 있도록 해주었다.
(반환할 내용이 false 이거나 json()을 주는건데 이 부분은 서로 타입이 일치하지 않아서... 맞게끔 수정해야겠다)

아무튼 로그인이 성공하면 category.html을 불러올 수 있도록 해주었다.

결과

확인 버튼을 통해 Fetch로 로그인 API 호출 및 응답에 따른 브라우저 처리도 잘 되고 있다..

3. 마무리

Fetch를 통해 API를 호출하고 렌더링하는 것은 제대로 사용하지 못해 공부하면서 하느라 시간이 많이 소요되었다..

다음은 로그인을 하면 header를 통해 토큰을 전달하고 사용하는 것까지 정리해볼 예정이다.

profile
정의로운 사회운동가

0개의 댓글