Instagram Clone : Frontend - part 2 [ LOGIN & SIGN UP]

정관우·2021년 9월 21일
0
post-thumbnail

LOGIN AND SIGNUP

Login UI

Extend

공통된 CSS 속성이 반복되는 경우, 속성의 묶음을 하나의 컴포넌트로 선언해준 다음 해당 스타일 컴포넌트를 다른 컴포넌트에게 상속시킬 수 있다. 공통된 스타일이 많을 경우 따로 저장하는 폴더를 만드는 것이 좋다. 방법은 다음과 같다.

// 같은 흰색 배경에 회색 테두리를 가지는 박스
const WhiteBox = styled.div`
  background-color: white;
  border: 1px solid rgb(219, 219, 219);
  width: 100%;
`;

// 위의 속성을 그대로 가져와 padding 등 다른 속성 추가
const TopBox = styled(WhiteBox)`
	padding: 35px 40px 20px 40px;
	...
`;

const BottomBox = styled(WhiteBox)`
	padding: 20px 0px;
	...
`;

Nesting

컴포넌트 안에 있는 특정 태그를 타겟으로 하여 CSS 속성을 부여할 수 있다. 해당 컴포넌트에서만 사용될 경우, 번거롭게 따로 컴포넌트를 선언할 필요없다.

// BottomBox의 자식 엘리먼트 중 a태그
const BottomBox = styled(WhiteBox)`
	...
  a {
    font-weight: 600;
    color: #0095f6;
  }
`;

Font 설정

Google Fonts

구글 폰트에서 원하는 폰트를 선택해준 다음 생성된 링크를 index.html의 head 부분에 붙여넣기 한다.

// index.html
<head>
...
	<link rel="preconnect" href="https://fonts.googleapis.com" />
	<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
	<link
	  href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600&display=swap"
	  rel="stylesheet"
	/>
...

FontAwesomeIcon 사용

FontAwesome을 import 해온 후, icon과 size를 넣어주면 쉽게 사용 가능하다. icon 입력 시, fa만 적어주면 목록이 자동 완성되기 때문에, 매우 편하게 사용할 수 있다.

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
  faFacebookSquare,
  faInstagram,
} from "@fortawesome/free-brands-svg-icons";

...
<FontAwesomeIcon icon={faInstagram} size="3x" />

Cleaning Login Code

중복 코드 제거

컴포넌트화

중복되는 여러 묶음의 CSS 속성이 있다면, 하나의 컴포넌트로 만들어주는 것이 좋다. 하지만, CSS가 중복될지 한 번만 사용될지 잘 모른다. 그래서, 모든 CSS 속성들을 무조건 컴포넌트로 만들기보다는 일단 Nesting을 해준 다음 중복되면 따로 컴포넌트로 만드는 것이 좋다.

테마화 (Theme)

여러 번 사용되는 속성 값들 ( color, border, background 등..)은 Theme으로 설정해준 다음, 컴포넌트를 만들 때 불러올 수 있다. 하나의 속성으로 적용되는 모든 CSS를 한꺼번에 변경할 수 있기 때문에 매우 유용하다.

// style.ts
...
export const lightTheme: DefaultTheme = {
  accent: "#0095f6",
  borderColor: "rgb(219, 219, 219)",
}; 

// Login.tsx
const WhiteBox = styled.div`
  border: 1px solid ${(props) => props.theme.borderColor};
	...
`;

Sign Up Routing

a 태그는 브라우저를 새로고침하기 때문에, 기존 React의 state를 모두 잃게된다. 그래서, React App에 머문 상태로 컴포넌트 간에 이동하기 위해 Link 를 이용해야 한다. Link는 HTML 태그로 변환할 때, a 태그를 리턴하기 때문에 a 태그로 준 CSS가 적용된다.

// Login.tsx
<Link to="/sign-up">Sign Up</Link>  

SignUp Route

Sign Up 컴포넌트는 로그인 유저가 접근하지 못하게 (Public-Only)가 되어야 한다. 다음과 같이 Routing해준다.

// App.tsx
{!isLoggedIn && (
	<Route path="/sign-up">
		<SignUp />
	</Route>
)}

Shared Components

파일 나누기

중복되는 UI를 따로 파일로 분리하여, 필요한 부분에 재사용하면 코드를 효율적으로 관리할 수 있다. Components 폴더에서 어디에서 쓰이는지에 따라 하위 폴더를 만들어 코드를 분리하여 저장한다. 이번 섹션에서 코드를 다음과 같이 분리하였다.

> src
	> Components
		> auth // 로그인 & 로그아웃에 필요한 컴포넌트
			- AuthLayout.tsx
			- BottomBox.tsx
			- Button.tsx
			- FormBox.tsx
			- Input.tsx
			- Seperator.tsx
		- Shared.tsx // 위 컴포넌트 안에서 또 중복되는 컴포넌트
- routes.ts // Route를 불러올 수 있음

Routes

Routes만 관리해주는 파일을 따로 만들어 URL 때문에 하는 실수를 사전에 방지할 수 있다.

// routes.ts
const routes = {
  home: "/",
  signUp: "/sign-up",
};
...

// App.tsx
...
<Route path={routes.signUp}>
...

Layout

페이지가 비슷한 레이아웃을 가지고 있을 때, 전체적인 구조를 잡는 여러 컴포넌트(flexbox, width 등..)가 중복될 수 있다. 이런 컴포넌트를 또 다른 컴포넌트로 묶는 것도 가능하다. 여기서, **children**이라는 모든 React 컴포넌트가 갖는 props를 사용해야한다. **children**로 컴포넌트의 모든 자식 컴포넌트를 불러올 수 있다.

// AuthLayout.tsx
...

// *** children의 타입 ***
type Props = {
  children: React.ReactNode;
};

function AuthLayout({ children }: Props) {
  console.log(children);
  return (
    <Container>
      <Wrapper>{children}</Wrapper>
    </Container>
  );
}
...

// Login.tsx
...
function Login(){
	return (
		<AuthLayout>
			<FormBox>
				...
			</FormBox>
			<BottomBox>
				...
			</BottomBox>
		</AuthLayout>
...

console.log(children)의 결과, 자식 컴포넌트인 FormBoxBottomBox가 차례로 찍힌 것을 볼 수 있다. 이 원리로 Container - Wrapper 구조를 매번 복붙하지 않고 AuthLayout 하나로 구현할 수 있다.

Shared Component

버튼이나 입력 창 같이 반복적으로 사용되는 같은 컴포넌트는 파일을 분리하여 Functional Component로 만들어준다. Functional Component를 만든 후, props를 내려받아 Styled Component로 다시 내려주는 방식으로 shared component를 구현할 수 있다.

// Login.tsx
...
<Button type="submit" value="Log in">
...

// Button.tsx

// props 타입화
type Props = {
  type: string;
  value: string;
};

const SButton = styled.input`
  ...
`;

// Login에서 Button에 내려준 props를 SButton(S : Styled)으로 전달
function Button(props: Props) {
  return <SButton {...props} />;
}
...

그런데, 위와 같이 function 안에서 어떤 작업을 하지 않는다면 굳이 functional component를 만들 필요는 없다.

// Button.tsx
const Button = styled.input`
  ...
`;

export default Button;

컴포넌트에서 따로 입력해야 할 부분들을 props로 받아 같은 스타일이지만 다른 내용을 리턴해주는 컴포넌트를 만들 수도 있다.

// Login.tsx
...
<BottomBox
	cta="Don't have an account?"
	link={routes.signUp}
	linkText="Sign Up"
/>
...

// BottomBox.tsx
...
const SBottomBox = styled(BaseBox)`
  ...
`;

/* 입력해야 할 부분들을 props로 받아 
같은 스타일이지만 다른 내용을 리턴해주는 컴포넌트를 만들어준다. */
function BottomBox({ cta, link, linkText }: Props) {
  return (
    <SBottomBox>
      <span>{cta}</span>
      <Link to={link}>{linkText}</Link>
    </SBottomBox>
  );
}
...

위의 컴포넌트 안에서도 반복되는 더 작은 단위의 컴포넌트가 존재한다. 이런 컴포넌트는 shared.ts 파일에 모두 저장시켜 모든 컴포넌트에서 매번 재사용이 가능하게 한다.

// shared.ts
...
export const BaseBox = styled.div`
  background-color: white;
  border: 1px solid ${(props) => props.theme.borderColor};
  width: 100%;
`; 
...

Sign Up UI

propTypes

propTypes란?
PropTypes는 부모로부터 전달받은 prop의 데이터 type을 검사한다. 자식 컴포넌트에서 명시해 놓은 데이터 타입과 부모로부터 넘겨받은 데이터 타입이 일치하지 않으면 콘솔에 에러 경고문이 띄워진다. 타입스크립트는 컴파일 단계에서 type을 체크하지만 propTypes는 런타임에서 한다는 차이가 있다.

// BottomBox.ts
function BottomBox({ cta, link, linkText }: Props) {
	...
}

BottomBox.propTypes = {
  cta: PropTypes.string.isRequired,
  link: PropTypes.string.isRequired,
  linkText: PropTypes.string.isRequired,
};

Sign Up UI

회원가입은 로그인과 거의 똑같이 생겼기 때문에, 만들어 놓았던 shared components에 props만 변경해주면 완성된다. 입력 창 위에 문구만 추가해주었다.

// shared.ts
// 굵은 글씨
export const FatLink = styled.span`
  font-weight: 600;
  color: rgb(142, 142, 142);
`;

// SignUp.tsx
// FatLink extend
// 굵은 글씨에 정렬, 폰트 사이즈, margin만 추가
const Subtitle = styled(FatLink)`
  text-align: center;
  font-size: 16px;
  margin-top: 10px;
`;

function SignUp() {
  return (
    ...
          <Subtitle>
            Sign up to see photos and videos from your friends.
          </Subtitle>
        </HeaderContainer>
        <form>
          <Input type="text" placeholder="Email" />
          <Input type="text" placeholder="Name" />
          <Input type="text" placeholder="Username" />
          <Input type="password" placeholder="Password" />
          <Button type="submit" value="Sign up" />
        </form>
      </FormBox>
      <BottomBox cta="Have an account?" link={routes.home} linkText="Log in" />
...

Helmet Component

React Helmet Async

브라우저 탭의 타이틀 부분을 손쉽게 조작할 수 있다. 사용 방법은 매우 간단하다.

Helmet 컴포넌트 안에 title 태그와 함께 제목을 넣어주면 된다. Helmet 컴포넌트는 HelmetProvider 컴포넌트 안에 있어야한다.

// App.tsx
...
return (
<HelmetProvider>
	...
</HelmetProvider>
)

// Login.tsx
...
return(
<AuthLayout>
	<Helmet>
		<title> Login | Instaclone </title>
	</Helmet>
...

하지만, 일일이 모든 컴포넌트에 중복되는 코드를 적어주기 번거롭기 때문에, 따로 컴포넌트화를 시켜주면 좋다.

// PageTitle.tsx
...
function PageTitle({ title }: Props) {
  return (
    <Helmet>
      <title>{title} | Instaclone</title>
    </Helmet>
  );
}

// Login.tsx
...
return(
<AuthLayout>
	<PageTitle title="Login" />
...

Forms in React

React로만 Form 만들기

React App을 만들면서 가장 번거로운 부분이 Form 만들기다. 신경 써야할 부분들이 매우 많다.

보통 다음과 같은 방식으로 React에서 Form을 만든다.

  1. Input과 입력 값을 State로 저장한다.
  2. onChange 이벤트로 Input이 입력될 때마다 setState로 상태를 변경한다.
  3. Validation을 위해, Submit 버튼에 여러 조건들을 걸어준다.
  4. 조건에 맞지 않을 경우, 에러 메시지를 띄워주기 위해 또 다른 상태를 만들어준다.
  5. 이 과정을 모든 항목 (ID, PW, Email 등..)에 대해 반복해준다.

이와 같이, Form 하나에 만들어야 하는 State도 너무 많고 복잡하다. React Hook Form을 이용하여 이를 훨씬 쉽게 구현할 수 있다.

React Hook Form

Basics

React에서 Form을 가장 쉽게 만들 수 있는 라이브러리다. 간단한 사용법은 다음과 같다.

우선, 모든 것은 useForm이라는 하나의 hook으로부터 온다.

useForm에서 register을 가져와 input에 추가한다. register은 기본 React로 Form을 만들 때, 거치는 과정 ( input의 state 생성 → onChange 이벤트 걸기 → input value 설정) 을 한번에 해결해준다.

register안에 name을 주어 어떤 input이 무엇에 사용되는지 React에게 알려주어야 한다.

<Input {...register([input의 name])} type="text" placeholder="Username" />

console.log(watch()) 를 사용하면, Input의 상태를 지속적으로 확인할 수 있다. Input의 상태가 변할 때 마다, Input의 name과 value가 { key : value }형태로 출력되는 것을 볼 수 있다.

Validation

유효성 검사도 훨씬 더 간단한 방법으로 구현할 수 있다. register의 두 번째 인자부터 option이 붙게 되는데, 여기서 원하는 유효성 검사를 입력하면 된다. required는 필수 입력인데, true로 설정하고 입력하지 않으면 그냥 submit이 되지 않고, true가 아닌 메시지를 입력하면 에러 메시지를 리턴할 수 있다.

다른 유효성 검사 (min, max, minLength , maxLength 등..)도 비슷하게 그냥 숫자를 넣으면, 해당 수 만큼의 유효성 검사를 한다. 대신, valuemessage를 넣게 넣으면 value의 검사를 한 후 통과하지 못하면 message를 에러로 리턴한다.

validate 옵션으로 콜백 함수를 넣는다면, 해당 함수가 true일 때만 submit이 가능하다. pattern 옵션을 넣으면 정규표현식을 사용할 수 있다.

useForm의 인자로 mode를 설정하면 유효성 검사를 진행하는 시점을 정할 수 있다.

  1. onSubmit - Form을 submit한 시점
  2. onChange - input의 값이 바뀔 때 마다
  3. onBlur - input의 focus 상태가 해제된 순간
  4. onTouched - input을 클릭하는 순간부터 계속

handleSubmit

handleSubmitsubmit 버튼을 클릭했을 때, 실행하는 submit handler다. Form의 유효성 검사 결과에 따라, 다른 함수를 실행시킬 수 있다. 첫 번째 인자는 유효성 검사 통과 시, 두 번째 인자는 통과하지 못할 시에 실행하는 함수를 넣어주면 된다.

에러 띄우기

formState에서 Form의 에러 메시지에 접근할 수 있다. formState는 항상 존재하나, 에러가 없을 수도 있으므로 key 뒤에 "?"를 붙여준다. 이를 이용하여 에러가 생길 때마다, Input 밑에 에러 메시지를 띄워줄 수 있다.

에러 메시지는 자주 사용되니 컴포넌트로 만들어주면 좋다. 에러 메시지를 props로 전달하여 에러가 생길 때, 생성되는 컴포넌트를 다음과 같이 만들 수 있다.

// FormError.tsx
...
type Props = {
  message: string | undefined;
};

function FormError({ message }: Props) {
	// 에러가 없으면 null, 있으면 props로 받은 메시지를 리턴한다.
  return message === "" || !message ? null : <SFormError>{message}</SFormError>;
}
...

추가적으로, formStateisSubmitted, isValid 등 Form의 여러 상태를 확인하고 이에 따라 다른 동작들을 실행시킬 수 있다. isValid를 이용하여 유효성 검사를 통과 여부에 따라, propsboolean 값을 내려주어 UI에 변화를 줄 수도 있다.

// Button.tsx
...
const Button = styled.input`
	...
	** props로 내려준 disabled에 따라 opacity 변경 (활성화 : 비활성화 효과 )
	opacity: ${(props) => (props.disabled ? "0.2" : "1")};
`;
...

// Input.tsx
...
interface SInput {
  hasError?: boolean;
}

const Input = styled.input<SInput>`
	...
  ** 에러 메시지가 있으면 hasError를 내려줌 (테두리 효과)
  border: 0.5px solid
    ${(props) => (props.hasError ? "tomato" : props.theme.borderColor)};
	...
  &:focus {
    border-color: rgb(38, 38, 38);
  }
`;

예시 코드

// login.tsx

interface IForm {
  username: string;
  password: string;
}

function Login() {
  const { register, handleSubmit, formState } = useForm<IForm>({
    mode: "onBlur",
  });

  const onSubmitValid: SubmitHandler<IForm> = (data) => {
    // Do Something...
  };

  return (
    ...
        <form onSubmit={handleSubmit(onSubmitValid)}>
          <Input
            {...register("username", {
              required: "Username is required",
              minLength: {
                value: 5,
                message: "Username should be longer than 5",
              },
            })}
            type="text"
            placeholder="Username"
            hasError={Boolean(formState.errors?.password?.message)}
          />
          {<FormError message={formState.errors?.username?.message} />}
          <Input
            {...register("password", { required: "Password is required" })}
            type="password"
            placeholder="Password"
            hasError={Boolean(formState.errors?.password?.message)}
          />
          {<FormError message={formState.errors?.password?.message} />}
          <Button type="submit" value="Log in" disabled={!formState.isValid} />
        </form>
		...

Apollo Client

Set Up

Apollo Client를 생성한다. uri에는 서버 주소를 입력하여 서버와 연결한다. cache는 Apollo가 가져온 정보를 기억하여 매번 같은 정보를 저장하지 않도록 로컬 환경에 저장한다.

// apollo.ts
...
export const client = new ApolloClient({
  uri: "http://localhost:4000/graphql",
  cache: new InMemoryCache(),
}); 

App 전체를 ApolloProvide로 감싸주면 컴포넌트 내에서 서버와 통신이 가능해진다. 위에서 만든 client를 props로 내려준다.

// App.tsx
...
return(
<ApolloProvider client={client}>
...

Login

Login Mutation

const LOGIN_MUTATION = gql`
	// Back-end로 가지 않는 코드 - mutation 이름은 상관 없음
  mutation login($username: String!, $password: String!) {
    login(username: $username, password: $password) {
      ok
      token
      error
    }
  }
`;
* username, password를 String으로 필수로 받아 ok, token, error를 응답으로 받음

useMutation

Mutation을 사용하기 위해 useMutation hook을 사용해야한다. 다음과 같은 값을 인자로 받는다.

const [mutationFn, {loading, data, called}] 
= useMutation(mutationName, {onComplete : onCompleteFn});
  1. mutationFn → mutation을 활성화시키는 함수
  2. loading → mutation 요청 중 : true / mutation 요청 완료 : false
  3. data → mutation 종료 이후 전달 받은 응답 데이터
  4. called → mutation이 호출됐는지 여부
  5. onComplete → Mutation이 완료됨과 동시에 가져온 데이터를 인자로 받는 콜백함수 실행

Form이 성공적으로 submit 됐을 때, getValues로 Form의 value를 가져와 로그인 요청을한다. 만약, mutation이 loading (요청 중)이면, 로그인 요청을 다시 하지 못하도록 한다. loading을 이용하여 요청 중인 상태를 UI로 표시해줄 수 있다.

// login.tsx
const onSubmitValid: SubmitHandler<IForm> = () => {
    if (loading) {
      return;
    }
    const { username, password } = getValues();
    login({ variables: { username, password } });
  };
...
return (
		...
		<Button
      type="submit"
      value={loading ? "Loading..." : "Log in"}
      disabled={!formState.isValid || loading}
    />
		...

요청이 완료되면 onComplete에 넣어준 콜백 함수가 실행된다. 여기서 setError를 사용하면 내가 원하는대로 에러 핸들링이 가능하다. 서버에서 보내온 에러를 메시지로 다음과 같이 띄울 수 있다.

// login.tsx
...
const onCompleted = (data: login) => {
    const {
      login: { ok, error, token },
    } = data;
    if (!ok && error) {
      // setError(에러 이름, 에러 옵션)
      return setError("result", { message: error });
			// -> 서버에서 보낸 에러를 formState.result에 추가 
    }
...
return (
...
	{<FormError message={formState.errors?.result?.message} />}
	// formState.errors.result가 생기면 에러 메시지를 Form 하단에 띄움
	</form>
...

하지만 여기서 버그가 발생한다. 에러가 발동하게 되면 Form이 invalid한 상태로 남게되면서 위의 submit 버튼이 disabled 되어 버튼을 다시 누를 수 없게 된다. 에러를 삭제하기 위해서 clearErrors를 사용한다. 유저가 다시 Form을 입력할 경우, 에러를 삭제하여 invalid한 상태를 풀어줄 수 있다.

// login.tsx
const clearLoginError = () => {
    // argument X => 모든 에러 삭제
    // argument "[input name]"" => input에 대한 에러 삭제
    clearErrors("result");
};

return (
				...
          <Input
            {...register("username", {
						...
            onFocus={clearLoginError}
          />
					...
          <Input
            {...register("password", { required: "Password is required" })}
						...
            onFocus={clearLoginError}
          />
					...

Handle Token

이제 로그인 요청이 성공하면 서버로부터 Token을 받아올 수 있다. 이 Token을 저장한 후 로그인 처리를 해주면 완성이다. 로그인과 로그아웃은 Reactive Variables를 사용하여 어디서든 호출할 수 있도록 하면 좋다.

localStorage에 Token을 저장하여 새로고침 시에도 Token이 유지되도록 한다. 하지만, 로그인 상태를 판별하는 변수 (isLoggedInVar)의 디폴트가 false이기 때문에 새로고침 시에 로그인 처리가 되지 않는다. 이를 해결하기 위해, localStorage.getItem(TOKEN)을 Boolean으로 변환하여 값이 존재하면 true를 아니면 false를 리턴하도록 한다.

// apollo.ts
...
const TOKEN = "token";
export const isLoggedInVar = makeVar(Boolean(localStorage.getItem(TOKEN)));

export const logUserIn = (token: string) => {
  localStorage.setItem(TOKEN, token);
  isLoggedInVar(true);
};

export const logUserOut = () => {
  localStorage.removeItem(TOKEN);
  isLoggedInVar(false);
};
...

Create Account

Login과 거의 흡사해서 따로 정리할 것은 없었다.

mutationFn Variables

handleSubmit의 첫번째 인자로 오는 콜백함수는 모든 항목의 유효성을 보장받기 때문에 서버로부터 받은 data를 spread operator로 variables에 넣어주어도 무방하다.

// SignUp.tsx
...
const onSubmitValid: SubmitHandler<IForm> = (data) => {
	...
    createAccount({
      variables: {
        ...data,
      },
	...
return (
	...
		<form onSubmit={handleSubmit(onSubmitValid)}>
	...

Redirecting Users

useHistory

react-router-dom의 useHistory를 사용하여, 강제로 유저를 다른 route로 이동시킬 수 있다. 회원가입 후, 로그인 페이지로 보낼 때 유용하다. 두 번째 인자로 이동시킨 컴포넌트에 state를 전달할 수도 있다.

// signUp.tsx
...
function SignUp() {
  const history = useHistory();
	const onCompleted = (data: createAccount) => {
		...
    history.push(routes.home, {
			message : "Account Created ~",
			username,
			password, 
		});
  };
	...

useLocation

history.push로 전달한 state는 useLocationlocation.state로 가져올 수 있다. 회원가입에서 생성된 아이디와 패스워드를 useFormdefaultValues로 설정하면 바로 로그인이 가능하다.

// Login.tsx
...
interface LocationState {
  message: string;
  username: string;
  password: string;
}
...
function Login() {
  const location = useLocation<LocationState>();
	const {...} = useForm<IForm>({
	    defaultValues: {
	      username: location?.state?.username || "",
	      password: location?.state?.password || "",
	    },
			...
	  });
	...
	return (
		...
		<Notification>{location?.state?.message}</Notification>
		...
	)

로그아웃 시에 남아있는 state를 지워주면, 깔끔하게 로그아웃 처리가 된다.

// apollo.ts
...
export const logUserOut = () => {
  localStorage.removeItem(TOKEN);
  window.location.reload();
};
...

Dark Mode

Dark Mode

Dark Mode를 활성화 / 비활성화 시킬 버튼을 만들어준다. 클릭 시에 Reactive Variables의 darkModeVar의 값을 변경해주는 함수를 클릭 이벤트로 넣어준다. Dark Mode 상태는 localStorage에 저장해주고 Logind의 Token과 마찬가지로, 저장한 값이 존재할 때 Dark Mode를 true로 없으면 false로 기본 값을 설정하도록 한다.

    // AuthLayout.tsx
    ...
    const darkMode = useReactiveVar(darkModeVar);
    return(
    	...
    	<Footer>
    		<DarkModeBtn onClick={darkMode ? disableDarkMode : enableDarkMode}>
           <FontAwesomeIcon icon={darkMode ? faSun : faMoon} />
        </DarkModeBtn>
      </Footer>
    	...

    // apollo.ts
    ...
    export const darkModeVar = makeVar(Boolean(localStorage.getItem(DARK_MODE)));

    export const enableDarkMode = () => {
      localStorage.setItem(DARK_MODE, "enabled");
      darkModeVar(true);
    };

    export const disableDarkMode = () => {
      localStorage.removeItem(DARK_MODE);
      darkModeVar(false);
    };
    ...

    // styles.ts
    // GlobalStyle과 theme을 연결
    export const GlobalStyles = createGlobalStyle`
    		...
        body {
            background-color: ${(props) => props.theme.bgColor};
            color: ${(props) => props.theme.fontColor};
    		...
    `;

    // shared.ts
    export const BaseBox = styled.div`
      background-color: ${(props) => props.theme.bgColor};
      border: 1px solid ${(props) => props.theme.borderColor};
    	...
    `;

    // App.tsx
    ...
    const darkMode = useReactiveVar(darkModeVar);
    return(
    	...
    	// darkMode 상태가 변경될 때, props.theme이 변경
    	<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
    ...
profile
작지만 꾸준하게 성장하는 개발자🌳

0개의 댓글