0425 - 0431
0425
외부 API 연동
데이터 연동하기
컴포넌트가 화면에 보이는 시점에 API를 요청할 경우, useEffect를 사용하여 컴포넌트가 처음 렌더링되는 시점에 API를 요청하면 된다.
주의할 점은 useEffect에 등록하는 함수에 async를 붙이면 안 된다는 것이다. useEffect에서 반환해야 하는 값은 뒷정리 함수이기 때문이다.
useEffect에서 async/await를 사용하고 싶다면, 함수 내부에 async 키워드가 붙은 또 다른 함수를 만들어서 사용해 주어야 한다.
const NewsList = () => {
const [articles, setArticles] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await axios.get(
"https://newsapi.org/v2/top-headlines?country=kr&apiKey=f45fa97679204f119d4d91f9f341c2fb"
);
setArticles(response.data.articles);
} catch (e) {
console.log(e);
}
setLoading(false);
};
fetchData();
}, []);
if (loading) {
return <NewsListBlock>대기 중...</NewsListBlock>;
}
if (!articles) {
return null;
}
return (
<NewsListBlock>
{articles.map((article) => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
};
- 데이터를 불러와서 컴포넌트 배열로 변환할 때 신경 써야 할 부분은 map 함수를 사용하기 전에 꼭 !articles를 조회하여 해당 값이 현재 null이 아닌지 검사해야 한다.
- 그렇지 않으면, 데이터가 없을 때 null에는 map 함수가 없기 때문에 렌더링 과정에서 오류가 발생한다.
리액트 라우터 적용
src 디렉터리에 pages를 생성하고, 파일을 만들어 작성한다.
const NewsPage = () => {
const { category = "all" } = useParams();
return (
<>
<Categories />
<NewsList category={category} />
</>
);
};
여기서 useParams 훅을 사용해 category params를 가져온다.
이후 App에서 Route를 정의한다.
const App = () => {
return (
<Routes>
<Route path=":category" element={<NewsPage />} />
<Route path="" element={<NewsPage />} />
</Routes>
);
};
v5 버전까지는 ?를 사용해서 선택적 params를 구현할 수 있었지만, v6 버전부터는 지원하지 않아 이와 같이 구현하도록 한다고 한다.
마지막으로 기존 Categories 컴포넌트를 NavLink 컴포넌트로 바꾼다.
const Categories = () => {
return (
<CategoriesBlock>
{categories.map((c) => (
<Category
key={c.name}
className={({ isActive }) => (isActive ? "active" : "")}
to={c.name === "all" ? "/" : `/${c.name}`}
>
{c.text}
</Category>
))}
</CategoriesBlock>
);
};
v5 버전과 달리 activeClass를 v6에서는 이와 같이 구현한다.
usePromise 커스텀 Hook
import { useEffect, useState } from "react";
export default function usePromise(promiseCreateor, deps) {
const [loading, setLoading] = useState(false);
const [resolved, setResolved] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const process = async () => {
setLoading(true);
try {
const resolved = await promiseCreateor();
setResolved(resolved);
} catch (e) {
setError(e);
}
setLoading(false);
};
process();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return [loading, resolved, error];
}
- 프로젝트의 다양한 곳에서 사용될 수 있는 유틸 함수들은 lib 디렉터리에 만든 후 작성한다.
- usePromise Hook은 Promise의 대기 중, 완료 결과, 실패 결과에 대한 상태를 관리하며, 의존 배열을 파라미터로 받아 온다.
- 받아온 deps 배열은 usePromise에서 의존 배열로 설정한다.
const NewsList = ({ category }) => {
const [loading, response, error] = usePromise(() => {
const query = category === "all" ? "" : `&category=${category}`;
return axios.get(
`https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=f45fa97679204f119d4d91f9f341c2fb`
);
}, [category]);
if (loading) {
return <NewsListBlock>대기 중...</NewsListBlock>;
}
if (!response) {
return null;
}
if (error) {
return <NewsListBlock>에러 발생!</NewsListBlock>;
}
const { articles } = response.data;
return (
<NewsListBlock>
{articles.map((article) => (
<NewsItem key={article.url} article={article} />
))}
</NewsListBlock>
);
};
- 코드가 이전보다 훨씬 간결해진 것을 확인할 수 있다.
- 이와 같이 커스텀 훅을 만들어 사용하면 좋은 코드를 만들어 갈 수 있다.
0426
Context API
Context API는 리액트 프로젝트에서 전역적으로 사용할 데이터가 있을 때 유용한 기능이다.
Context를 사용하면 컴포넌트 트리로 묶인 컴포넌트 간 데이터 공유가 비교적 수월해진다.
단, Context는 컴포넌트를 재사용하기 어렵게 만드므로 꼭 필요한 경우만 사용하는 것이 좋다.
ex) 사용자 로그인 정보, 애플리케이션 환경 설정, 테마 등
전역 상태를 관리할 때 리액트 프로젝트에서 많은 컴포넌트를 거쳐야 할 때도 있고 다루어야 하는 데이터가 훨씬 많아질 수 있으므로 props를 전달하는 방식으로 사용하면 유지 보수성이 낮아질 가능성이 있다.
Context API를 사용하면 Context를 만들어 단 한 번에 원하는 값을 받아와 사용할 수 있다.
새 Context 만들기
새 Context를 만들 때는 createContext 함수를 사용한다.
파라미터에는 해당 Context의 기본 상태를 지정한다.
import { createContext } from "react";
const ColorContext = createContext({ color: "black" });
export default ColorContext;
Consumer 사용
새로운 컴포넌트를 만들어서 Context 안에 있는 값을 사용할 때는 props로 받아 오는 것이 아니라 Context 안에 들어 있는 Consumer라는 컴포넌트를 통해 값을 사용한다.
const ColorBox = () => {
return (
<ColorContext.Consumer>
{(value) => (
<div
style=
/>
)}
</ColorContext.Consumer>
);
};
- Consumer 사이에 중괄호를 열어서 그 안에 함수를 넣어 주었다.
- 이러한 패턴을 Function as a Child, 혹은 Render Props라고 한다. 컴포넌트의 children이 있어야 할 자리에 일반 JSX 혹은 문자열이 아닌 함수를 전달하는 것이다.
Render Props 예시
const RenderPropsSample = ({ children }) => {
return <div>결과: {children(5)}</div>;
};
// 사용
<RenderPropsSample>{(value) => 2 * value}</RenderPropsSample>;
Provider
Provider를 사용하면 Context의 value를 변경할 수 있다.
function App() {
return (
<ColorContext.Provider value=>
<div>
<ColorBox />
</div>
</ColorContext.Provider>
);
}
- Provider를 사용할 때는 value 값을 명시해 주어야 제대로 작동한다.
동적 Context 사용하기
Context의 value에는 무조건 상태 값만 있어야 하는 것은 아니다. 함수를 전달해 줄 수도 있다.
const ColorContext = createContext({
state: { color: "black", subcolor: "red" },
actions: {
setColor: () => {},
setSubcolor: () => {},
},
});
const ColorProvider = ({ children }) => {
const [color, setColor] = useState("black");
const [subcolor, setSubcolor] = useState("red");
const value = {
state: { color, subcolor },
actions: { setColor, setSubcolor },
};
return (
<ColorContext.Provider value={value}>{children}</ColorContext.Provider>
);
};
const { Consumer: ColorConsumer } = ColorContext;
export { ColorProvider, ColorConsumer };
- ColorProvider라는 컴포넌트를 새로 작성해 주었다. 그리고 이 컴포넌트에서는 ColorContext.Provider를 렌더링하고 있다.
- Provider의 value에는 상태를 state로, 업데이트 함수는 actions로 묶어서 전달하고 있다.
- Context에서 값을 동적으로 사용할 때 반드시 묶어서 줄 필요는 없지만, state와 actions 객체를 따로 분리해 주면 나중에 다른 컴포넌트에서 Context의 값을 사용할 때 편리하다.
- createContext를 사용할 때 기본값은 실제 Provider의 value에 넣는 객체의 형태와 일치시켜 주는 것이 좋다. 이렇게 하면 Context 코드를 볼 때 내부 값이 어떻게 구성되어 있는지 파악하기도 쉽고, 실수로 Provider를 사용하지 않았을 때 리액트 앱에서 에러가 발생하지 않는다.
useContext Hook 사용
리액트 내장 Hooks 중, useContext를 사용하면 함수 컴포넌트에서 Context를 아주 편하게 사용할 수 있다.
const ColorBox = () => {
const { state } = useContext(ColorContext);
return (
<>
<div style= />
<div
style=
/>
</>
);
};
- Render Props의 패턴보다, 훨씬 편하게 Context 값을 조회할 수 있다.
- useContext의 인자로 사용하고자 하는 Context를 넘겨 사용하면 된다.
static contextType 사용
클래스형 컴포넌트에서 Context를 좀 더 쉽게 사용하고 싶다면 static contextType을 정의하는 방법이 있다.
const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
class SelectColors extends Component {
static contextType = ColorContext;
handleSetColor = (color) => {
this.context.actions.setColor(color);
};
handleSetSubcolor = (subcolor) => {
this.context.actions.setSubcolor(subcolor);
};
render() {
return (
<div>
<h2>색상을 선택하세요.</h2>
<div style=>
{colors.map((color) => (
<div
key={color}
style=
onClick={() => this.handleSetColor(color)}
onContextMenu={(e) => {
e.preventDefault();
this.handleSetSubcolor(color);
}}
/>
))}
</div>
<hr />
</div>
);
}
}
- static contextType을 정의하면 클래스 메서드에서도 Context에 넣어 둔 함수를 호출할 수 있다.
- 하지만, 한 클래스에서 하나의 Context밖에 사용하지 못한다는 단점이 있다.
- 그러나 앞으로 클래스 컴포넌트를 작성하는 일은 많지 않기 때문에 useContext를 사용하는 쪽이 좋다.
결론
기존에는 컴포넌트 간 상태 교류를 무조건 부모에서 자식 흐름으로 전달했지만, 이제는 Context API를 통해 쉽게 상태를 교류할 수 있게 되었다.
하지만 프로젝트 컴포넌트 구조가 간단하고 상태의 종류가 많지 않다면, 굳이 Context를 사용할 필요는 없다. 전역적으로 여기저기서 사용되는 상태가 있고 컴포넌트의 개수가 많은 상황이라면, Context API를 사용하는 것이 권장된다.
0427
캐시 제어
HTTP는 문서가 만료되기 전까지 얼마나 오랫동안 캐시될 수 있게 할 것인지 서버가 설정할 수 있는 여러 가지 방법을 정의한다.
no-cache와 no-store 응답 헤더
no-store와 no-cache 헤더는 캐시가 검증되지 않은 캐시된 객체로 응답하는 것을 막는다.
Cache-Control: no-store
Cache-Control: no-chche
no-store가 표시된 응답은 캐시가 그 응답의 사본을 만드는 것을 금지한다.
no-cache로 표시된 응답은 로컬 캐시 저장소에 저장될 수 있지만, 먼저 서버와 재검사를 하지 않고서는 캐시에서 클라이언트로 제공될 수 없을 뿐이다.
Max-Age 응답 헤더
Cache-Control: max-age 헤더는 신선하다고 간주되었던 유효 시간을 의미하고, 초로 나타낸다.
서버는 최대 age를 0으로 설정함으로써, 캐시가 매 접근마다 문서를 캐시하거나 리프레시하지 않도록 요청할 수 있다.
Expires 응답 헤더
초 단위의 시간 대신 실제 만료 날짜를 명시한다. deprecated
Must-Revalidate 응답 헤더
Cache-Control: must-revalidate 응답 헤더는 캐시가 이 객체의 신선하지 않은 사본을 원 서버와의 재검사 없이는 제공해서는 안 됨을 의미한다.
만약 캐시가 신선도 검사를 시도했을 때 원 서버가 사용할 수 없는 상태라면, 캐시는 반드시 504를 반환해야 한다.
반면 Cache-Control: no-cache와의 차이점은 no-cache는 원 서버에 접근할 수 없는 경우 캐시 서버 설정에 따라 캐시 데이터를 반환할 수 있다.
0428
HTTP/2.0
HTTP/1.1은 커넥션 하나를 통해 요청 하나를 보내고 그에 대한 응답 하나만을 받는 HTTP의 메시지 교환 방식을 사용했다.
이는 단순했지만, 응답을 받아야만 그 다음 요청을 보낼 수 있기 때문에 심각한 회전 지연을 피할 수 없었다.
이 문제를 회피하기 위해 병렬 커넥션이나 파이프라인 커넥션이 도입되었지만 성능 개선에 대한 근본적인 해결책은 되지 못했다.
개요
HTTP/2.0 요청과 응답은 길이가 정의된 한 개 이상의 프레임에 담긴다. 이때 HTTP 헤더는 압축되어 담긴다.
프레임들에 담긴 요청과 응답은 스트림을 통해 보내진다. 한 개의 스트림이 한 쌍의 요청과 응답을 처리한다.
하나의 커넥션 위에 여러 개의 스트림이 동시에 만들어질 수 있으므로, 여러 개의 요청과 응답을 동시에 처리하는 것 역시 가능하다.
HTTP/1.1과의 차이점
프레임
HTTP/2.0에서 모든 메시지는 프레임에 담겨 전송된다.
모든 프레임은 8바이트 크기의 헤더로 시작하며, 뒤이어 최대 16383 바이트 크기의 페이로드가 온다.
HTTP/2.0은 총 10가지 프레임을 정의하고 있으며, 페이로드의 형식이나 내용은 프레임의 종류에 따라 다르다.
스트림과 멀티플렉싱
스트림은 HTTP/2.0 커넥션을 통해 클라이언트와 서버 사이에서 교환되는 프레임들의 독립된 양방향 시퀀스다.
한 쌍의 HTTP 요청과 응답은 하나의 스트림을 통해 이루어진다.
- 클라이언트는 새 스트림을 만들어 그를 통해 HTTP 요청을 보낸다.
- 요청을 받은 서버는 그 요청과 같은 스트림으로 응답을 보낸다.
- 그러고 나면 스트림이 닫히게 된다.
HTTP/1.1에서는 한 TCP 커넥션을 통해 요청을 보냈을 때, 그에 대한 응답이 도착하고 나서야 같은 TCP 커넥션으로 다시 요청을 보낼 수 있다.
따라서 웹브라우저들은 대기 시간을 줄이기 위해 여러 개의 TCP 커넥션을 만들어 동시에 여러 개의 요청을 보내는 방법을 사용한다. 그렇다고 TCP 커넥션을 무한정 만들 수는 없기에 대기 시간이 늘어나는 것을 피하기 어렵다.
그러나 HTTP/2.0에서는 하나의 커넥션에 여러 개의 스트림이 동시에 열릴 수 있다. 따라서 하나의 HTTP/2.0 커넥션을 통해 여러 개의 요청이 동시에 보내질 수 있기 때문에 이 문제는 쉽게 해결될 수 있다.
서버와 클라이언트는 스트림을 상대방과 협상 없이 일방적으로 만든다. 이는 스트림을 만들 때 협상을 위해 TCP 패킷을 주고받느라 시간을 낭비하지 않아도 됨을 의미한다.
헤더 압축
HTTP/1.1에서 헤더는 아무런 압축 없이 그대로 전송되었다.
요즈음에는 웹페이지 하나를 보기 위해 수십에서 많으면 수백 번의 요청을 보내기 때문에, 헤더의 크기가 latency와 대역폭 양쪽 모두에 실질적인 영향을 끼치게 되었다.
이를 개선하기 위해 HTTP/2.0에서는 HTTP 메시지의 헤더를 압축하여 전송한다.
서버 푸시
HTTP/2.0은 서버가 하나의 요청에 대해 응답으로 여러 개의 리소스를 보낼 수 있도록 해준다.
이 기능은 서버가 클라이언트에서 어떤 리소스를 요구할 것인지 미리 알 수 있는 상황에서 유용하다.
알려진 보안 이슈
중개자 캡슐화 공격
HTTP/2.0 메시지를 중간의 프락시가 HTTP/1.1 메시지로 변환할 때 메시지의 의미가 변질될 가능성이 있다.
HTTP/2.0은 헤더 필드의 이름과 값을 바이너리로 인코딩한다. 이는 헤더 필드로 어떤 문자열이든 사용할 수 있게 해준다.
이는 정상적인 HTTP/2.0 요청이나 응답이, 위조된 HTTP/1.1 메시지로 번역되는 것을 유발할 수 있다.
긴 커넥션 유지로 인한 개인정보 누출 우려
HTTP/2.0은 사용자가 요청을 보낼 때의 회전 지연을 줄이기 위해 클라이언트와 서버 사이의 커넥션을 오래 유지하는 것을 염두에 두고 있다.
이것은 개인 정보의 유출에 악용될 가능성이 있다.
0429
클라이언트 식별과 쿠키
개별 접촉
HTTP는 익명으로 사용하며 stateless하게 요청과 응답으로 통신하는 프로토콜이다.
웹 서버는 요청을 보낸 사용자를 식별하거나 방문자가 보낸 연속적인 요청을 추적하기 위해 정보를 이용할 수 있다.
현대의 웹 사이트들은 개인화된 서비스를 제공하고 싶어 한다.
ex) 개별 인사, 사용자 맞춤 추천, 저장된 사용자 정보, 세션 추적
HTTP 헤더
- from - 사용자의 이메일 주소를 포함한다.
- 악의적인 서버가 이메일 주소를 모아서 스팸 메일을 발송하는 문제가 있어서 From 헤더를 보내는 브라우저는 많지 않다.
- User-Agent - 사용자가 쓰고 있는 브라우저의 이름과 버전 정보를 포함한다.
- 특정 브라우저에서 제대로 동작하도록 콘텐츠 최적화 측면에서는 유용할 수 있지만, 특정 사용자 식별에는 도움이 되지 않는다.
- Referer - 사용자가 현재 페이지로 유입하게 한 웹페이지의 URL을 가리킨다.
- 사용자를 식별할 수는 없지만, 이 헤더를 통해서 사용자의 웹 사용 행태나 사용자의 취향을 더 잘 파악할 수 있다.
클라이언트 IP 주소
클라이언트 IP 주소로 사용자를 식별하는 방식은 다음과 같은 약점을 가진다.
- 클라이언트 IP 주소는 사용자가 아닌, 사용하는 컴퓨터를 가리킨다.
- 로그인한 시간에 따라, 매번 다른 주소를 받으므로 웹 서버는 사용자를 IP 주소로 식별할 수 없다.
- 보안을 강화하기 위해 많은 사용자가 네트워크 주소 변환 방화벽을 통해 인터넷을 사용한다.
- 웹 서버는 클라이언트의 IP 주소 대신 프락시 서버의 IP 주소를 볼 수도 있다.
-> 사용자 식별 방식으로 사용하기 힘들다.
사용자 로그인
사용자 이름과 비밀번호로 인증할 것을 요구해서 사용자에게 명시적으로 식별 요청을 할 수 있다.
HTTP는 WWW-Authenticate와 Authorization 헤더를 사용해 웹 사이트에 사용자 이름을 전달하는 자체적인 체계를 가지고 있다.
한 번 로그인하면, 브라우저는 사이트로 보내는 모든 요청에 이 로그인 정보를 함께 보내므로 웹 서버는 항상 로그인 정보를 확인할 수 있다.
뚱뚱한 URL
사용자의 상태 정보를 포함하고 있는 URL을 뚱뚱한 URL이라고 한다.
사용자가 웹 사이트에 처음 방문하면 유일한 ID가 생성되고, 그 값은 서버가 인식할 수 있는 방식으로 URL에 추가되며, 서버는 클라이언트를 이 뚱뚱한 URL로 리다이렉트 시킨다.
이러한 방식은 사용자를 식별할 수 있지만 여러 심각한 문제가 있다.
- 못생긴 URL
- 공유하지 못하는 URL
- 캐시를 사용할 수 없음
- 서버 부하 가중
- 이탈 및 세션 간 지속성의 부재
쿠키
쿠키의 타입
쿠키는 크게 세션 쿠키와 지속 쿠키로 나눌 수 있다.
세션 쿠키는 사용자가 브라우저를 닫으면 삭제된다.
반면 지속 쿠키는 디스크에 저장되어, 브라우저를 닫거나 컴퓨터를 재시작하더라도 남아있다.
세션 쿠키와 지속 쿠키의 다른 점은 파기되는 시점 뿐이다.
쿠키의 동작
처음 사용자가 웹 사이트에 방문하면 웹 서버는 사용자에 대해 아무것도 모른다.
웹 서버는 사용자가 다시 돌아왔을 때, 해당 사용자를 식별하기 위해 유일한 값을 쿠키에 할당한다.
쿠키는 set-Cookie 같은 HTTP 응답 헤더에 기술되어 사용자에게 전달한다.
브라우저는 헤더에 있는 쿠키 콘텐츠를 브라우저 쿠키 데이터베이스에 저장한다.
사용자가 미래에 같은 사이트를 방문하면, 브라우저는 서버가 이 사용자에게 할당했던 쿠키를 Cookie 요청 헤더에 기술해 전송한다.
사이트마다 다른 쿠키들
브라우저는 쿠키를 생성한 서버에게만 쿠키에 담긴 정보를 전달한다.
쿠키 Domain 속성
domain 속성은 어떤 사이트가 그 쿠키를 읽을 수 있는지 제어할 수 있다.
쿠키 Path 속성
웹 사이트 일부에만 쿠키를 적용할 수도 있다.
Path 속성을 기술해서 해당 경로에 속하는 페이지에만 쿠키를 전달한다.
쿠키와 세션 추적
쿠키는 웹 사이트에 수차례 트랜잭션을 만들어내는 사용자를 추적하는 데 사용한다.
전자상거래 웹 사이트는 사용자가 온라인 쇼핑을 하는 중에도 그들의 쇼핑카트를 유지하려 세션 쿠키를 사용한다.
쿠키와 캐싱
쿠키 트랜잭션과 관련된 문서를 캐싱하는 것은 주의해야 한다. 이전 사용자의 쿠키가 다른 사용자에게 할당돼버리거나, 누군가의 개인 정보가 다른 이에게 노출되는 최악의 상황이 일어날 수도 있다.
- 캐시되지 말아야 할 문서가 있다면 표시하라
- Set-Cookie 헤더를 캐시 하는 것에 유의하라
- Cookie 헤더를 가지고 있는 요청을 주의하라
기본 인증
웹 서버는 클라이언트의 요청을 거부하고 유효한 사용자 이름과 비밀번호를 요구할 수 있다.
서버는 401 상태 코드와 함께, 클라이언트가 접근하려고 했던 보안 영역을 WWW-Authenticate에 기술해서 응답하여 인증요구를 시작한다.
브라우저는 사용자가 입력한 사용자 이름과 비밀번호를 Authorization 요청 헤더 안에 암호화해서 서버로 다시 보낸다.
Base-64 사용자 이름/비밀번호 인코딩
base-64 인코딩은 8비트 바이트로 이루어져 있는 시퀀스를 6비트 덩어리의 시퀀스로 변환한다.
기본 인증에서는 사용자 이름과 비밀번호를 콜른으로 이어서 합치고, base-64 인코딩 메서드를 사용해 인코딩 한다.
Base-64 인코딩은 바이너리, 텍스트, 국제 문자 데이터 문자열 받아서 전송할 수 있게, 그 문자열을 전송 가능한 문자인 알파벳으로 변환하기 위해 발명했다.
프락시 인증
중개 프락시 서버를 통해 인증할 수도 있다.
프락시 서버에서 접근 정책을 중앙 관리 할 수 있기 때문에, 회사 리소스 전체에 대해 통합적인 접근 제어를 하기 위해서 프락시 서버를 사용하면 좋다.
프락시 인증은 웹 서버의 인증과 헤더와 상태 코드만 다르고 절차는 같다.
기본 인증의 보함 결함
- base-64로 인코딩한 비밀번호는 사실상 비밀번호 그대로 보내는 것과 다름없다. 이게 문제가 된다면, 모든 HTTP 트랜잭션을 SSL 암호화 채널을 통해 보내는 것이 좋다.
- 보안 비밀번호가 복잡한 방식으로 인코딩되어 있다고 하더라도, 캡처해서 그대로 원 서버에 보내서 인증에 성공할 수 있다.
- 메시지의 인증 헤더를 건드리지는 않지만, 그 외 다른 부분을 수정해서 프락시가 중간에 개입하는 경우, 기본 인증은 정상적인 동작을 보장하지 않는다.
- 기본 인증은 가짜 서버의 위장에 취약하다.