티스토리 뷰

2023년 10월, 원티드 프리온보딩 프론트엔드 챌린지 요약

 

로그인은 무엇일까?

로그인은 사용자가 시스템(컴퓨터 or 웹 사이트)에 액세스하기 위해 시스템에 식별자 정보를 입력하는 것이다. 이 때, 컴퓨터 보안 절차는 필수적인 부분이다.

 

해당 사용자는 시스템의 제한된 파일 및 프로그램 세트에만 액세스할 수 있다.

 

로그인은 액세스를 제한할 뿐만 아니라 시스템 로그 파일에 자동으로 입력되는 데이터 형식의 감사 추적도 제공한다.

 

즉, 쉽게 말해 "사용자가 시스템에 접근하거나 동작을 수행하는 것을 제어하고 기록하기 위한 컴퓨터 보안 절차"를 말한다.

주문, 리뷰, 쿠폰, 기타 등 회원정보가 필요한 서비스에는 로그인이 필요하다.

또한, 특정 서비스마다 접근 가능한 회원도 나눌 수 있다.

 

로그인 구현을 위한 개념 - 백엔드

  • 사용자 식별
  • 접근 및 동작 제어

 

로그인 구현을 위한 개념 - 프론트엔드

  • 권한이 없는 자원에 접근하지 않는 구조 만들기
    • 유저 전용 페이지 및 데이터
    • 사장 전용 페이지 및 데이터
  • 권한이 없는 자원에 접근하지 않는 구조 만들기
  • 권한이 없는 자원의 존재를 모르도록 하기
  • 로그인/로그아웃 만들기
  • 인증 정보 관리하기

 

로그인 구현을 위한 프론트엔드 개념 정리

  • 필요한 최소한의 구현 요구사항
    • 로그인 페이지
    • 로그인 인증관련 데이터 관리
    • 로그인 상태에 따른 화면/기능 제어
    • 로그아웃

 

로그인 화면 구현하기

로그인 화면을 구현하고 테스트하기 위해서 임의로 아이디와 비밀번호 그리고 회원정보를 작성한다.

구현할 때, 타입스크립트를 사용한다.

interface UserInfo {
  name: string;
}

interface User {
  username: string;
  password: string;
  userInfo: UserInfo;
}

const users: User[] = [
  {
    username: "blue",
    password: "1234",
    userInfo: { name: "blueStragglr" },
  },
  {
    username: "white",
    password: "1234",
    userInfo: { name: "whiteDwarf" },
  },
  {
    username: "red",
    password: "1234",
    userInfo: { name: "redGiant" },
  },
];

 

사용자 이름과 비밀번호를 입력하고 버튼을 눌렀을 때, 로그인이 동작되도록 코드를 작성한다.

입력한 사용자 이름과 비밀번호가 users 데이터에 있는지 없는지 여부를 파악하고 그에 맞는 결과를 내보낸다.

const _secret: string = "1234qwer!@#$";

const login = async (
  username: string,
  password: string
): Promise<LoginResponse | null> => {
  // TODO: 올바른 username, password를 입력하면 {message: 'SUCCESS', token: (원하는 문자열)} 를 반환하세요.
  const user: User | undefined = users.find((user: User) => {
    return user.username === username && user.password === password;
  });

  return user
    ? {
        message: "SUCCESS",
        token: JSON.stringify({ user: user.userInfo, secret: _secret }),
      }
    : null;
};

 

로그인이 성공했을 때, 회원정보를 나타낼 수 있도록 한다.

const getUserInfo = async (token: string): Promise<UserInfo | null> => {
  // TODO: login 함수에서 받은 token을 이용해 사용자 정보를 받아오세요.
  const parsedToken = JSON.parse(token);
  if (!parsedToken?.secret || parsedToken.secret !== _secret) return null;

  const loggedUser: User | undefined = users.find((user: User) => {
    if (user.userInfo.name === parsedToken.user.name) return user;
  });

  return loggedUser ? loggedUser.userInfo : null;
};

 

이제 입력폼에 아이디와 비밀번호를 입력한 값으로 로그인이 가능한 지, 회원정보가 있는지 최종적으로 판단하는 코드를 작성한다. 회원이 있다면, 그 회원의 회원정보가 있는지 판단하고 최종적으로 userInfo 변수 값에 저장한다. 없다면 null 값을 저장한다.

const [userInfo, setUserInfo] = useState<UserInfo>({ name: "" });

  const loginSubmitHandler = async (
    event: React.FormEvent<HTMLFormElement>
  ) => {
    event.preventDefault();

    // TODO: form 에서 username과 password를 받아 login 함수를 호출하세요.
    const formData = new FormData(event.currentTarget);

    const loginRes = await login(
      formData.get("username") as string,
      formData.get("password") as string
    );
    if (!loginRes) return;

    const userInfo = await getUserInfo(loginRes.token);
    if (!userInfo) return;

    setUserInfo(userInfo);
  };

 

로그인 페이지를 나타내자.

<div>
      <h1>Login with Mock API</h1>
      <form onSubmit={loginSubmitHandler}>
        {/* TODO: 여기에 username과 password를 입력하는 input을 추가하세요. 제출을 위해 button도 추가하세요. */}
        <label htmlFor="username">Username</label>
        <input type="text" id="username" name="username" />
        <label htmlFor="password">Password</label>
        <input type="password" id="password" name="password" />
        <button type="submit" value="Submit">
          Login
        </button>
      </form>
      <div>
        <h2>User info</h2>
        {/* TODO: 유저 정보를 보여주도록 구현하세요. 필요에 따라 state나 다른 변수를 추가하세요. */}
        {JSON.stringify(userInfo)}
      </div>
    </div>

 

과연 위의 코드를 실제로 사용할 수 있을까? 사용할 수는 없을 것이다.

회원정보가 그대로 노출되어 있고, 유효성 검사, 보안 등이 제대로 구현되어 있지 않다.

단순히 로그인 과정이 어떻게 이루어지는지 확인할 수 있는 정도이다.

 

위의 로그인 테스트는 "Mock API"로 구현되어 있다.

Mock API를 사용하게 되면 원하는 형태의 API 설계가 가능해진다. 그리고 서버 배포 없이 개발이 가능하다. 또한, 초기 인프라스트럭쳐 구성 비용이 절감되는 이점이 있다.

 

Mock API을 만드는 방법으로 다양한 라이브러리가 있다.

예. Mock Service Worker

import { setupWorker, rest } from 'msw'

interface LoginBody {
	username: string
}

interface LoginResponse {
	username: string
    firstName: string
}

const worker = setupWorker(
	rest.post<LoginBody, LoginResponse>('/login', async (req, res) => {
    	const { username } =await req.json()
        
        return res(
        	ctx.json({
            	username,
                firstName: 'John'
            })
        )
    }),
)

 

그렇다면, 실제 서비스에서 로그인이 실행되도록 하려면 어떤 고민을 해야 할까?

우리는 로그인의 정보를 담은 토큰을 어디에 저장할 것인지 고민해야 한다.

 

  • Cookies vs Local Storage vs Session Storage

 

저장한 토큰을 확인을 위해 매 번 직접 넣어주어야 할까? 이는 너무 불편하지 않을까?

위의 Mock API로 구현한 코드에서 확인해보면 토큰을 가져와서 사용하는 것을 볼 수 있다.

 

만약, 유저 정보를 다른 곳에서도 사용하고 싶다면 어떻게 해야 할까?

그 때마다 불러와서 사용해야 할까?

 

실제 서비스를 구성하는 '페이지는 로그인이 필요한 페이지'와 '그렇지 않은 페이지'로 구분된다.

 

로그인이 필요없는 페이지는 다음과 같이 구현될 것이다.

const PageWithoutLogin = () => {
	return <div>로그인이 필요없는 페이지</div>
}

export default PageWithoutLogin

 

반대로, 로그인이 필요한 페이지는 다음 코드가 필요할 것이다. 이 때, 로그인 여부로 커스텀 훅을 사용할 수 있다.

const PageWithLogin = () => {
	const { isLogged, routeToLoginPage } = useLoginState()
    
    if (!isLogged) {
    	routeToLoginPage()
        return <>로그인 페이지로 이동</>
    }
    
    return <div>로그인이 필요한 페이지</div>
}

 

그러나, 로그인이 필요한 페이지의 코드를 보면 useLoginState부터 조건문까지 반복되는 확인 코드와 로직을 발견할 수 있다.

로그인 여부 확인 로직을 모듈화하여 반복을 줄여보자.

interface AuthorizationProps {
	children: React.ReactNode
}

const Authorization: React.FC<AuthorizationProps> = ({ children: AuthorizationProps }) => {
	const { isLogged, routeToLoginPage } = useLoginState()
    
    if (!isLogged) {
    	routeToLoginPage()
        return <>로그인 페이지로 이동</>
    }
    
    return <>{children}</>
}

export default Authorization

 

모듈화한 컴포넌트는 페이지를 감싸는 컴포넌트로써 사용할 수 있다.

예. <Authorization><PageA /></ Authorization >

 

자, 이제 모듈화시킨 컴포넌트들을 사용해서 적용해보자.

const RouterInfo: RouterItem[] = [
	{
    	path: '/',
        element: <Home />,
        withAuthorization: true,
        label: '홈',
        icon: <PeopleIcon />,
    },
    {
    	path: '/page-a',
        element: <PageA />,
        withAuthorization: true,
        label: '페이지 A',
        icon: <DashboardIcon />,
    },
    {
    	path: '/page-b',
        element: <PageB />,
        withAuthorization: true,
        label: '페이지 B',
        icon: <AppRegistrationIcon />,
    },
]

export const ReactRouterObject: Router = createBrowserRouter(
	RouterInfo.map((routerInfo: RouterItem) => {
    	return routerInfo.withAuthorization
        	? {
            	path: routerInfo.path,
                element: (
                	<Authorization currentPath={routerInfo.path.replace(/\/\*$/g, '')}>
                    	{routerInfo.element}
                    </Authorization>
                )
            }
            : {
            	path: routerInfo.path,
                element: routerInfo.element,
            }
    })
)
<RouterProvider router={ReactRouterObject} />

페이지의 개수만큼 반복하여 로그인 여부에 따라 페이지를 출력하도록 구현되었다.

 

이제 메뉴 바 또는 사이드 바 정보를 생성해서 위의 구현한 내용들을 사용할 수 있다.

export const SidebarContent: SidebarItem[] = (() => 
	RouterInfo.reduce((prev: SidebarItem[], current: RouterItem) => {
    	if (current.withAuthorization)
        	return [
            	...prev,
                {
                	path: current.path,
                    label: current.label,
                    icon: current.icon,
                }
            ]
        return prev
    }, [] as SidebarItem[]))()

 

마지막으로 전체적인 레이아웃을 만들어보자.

const GeneralLayout: React.FC<AuthorizationProps> = ({children: React.ReactNode, withLogin: boolean | undefined}) => {
	const { isLogged, routeToLoginPage } = useLoginState()
    
    if (!isLogged && withLogin) {
    	routeToLoginPage()
        return <>로그인 페이지로 이동</>
    }
    
    return <div>
    		<nav>
            	Login Process
            </nav>
            <div>
            	<Sidebar content={SidebarContent} />
                <div>
                	{children}
                </div>
            </div>
    	</div>
}

 

이렇게 모든 페이지에서 반복되는 부분들을 쪼개고 쪼개어 모듈화시키는 과정을 연습해보자.

 

CSR과 SSR, 유지보수 관점에서 생각해보자

CSR과 SSR 중 현업에서는 어떤 것을 사용할까? 답은 진리의 케바케이다.

 

CSR과 SSR의 비교를 이미지로 쉽게 학습해보자.

 

둘의 선택 기준은 다음과 같은 질문에 따라 달라질 수 있다.

  • 추가로 서버가 필요한가?
  • SEO가 필요한가?
  • 어떤 유저 인터랙션이 많은가?
    • 페이지 이동 vs 페이지 내 인터랙션

 

만약, 주니어가 공부한다면 "CSR" → "SSR" → "인프라" 순서로 공부하는 것을 추천한다.