! calls out to a shell (in a new process), while % affects the process associated with the notebook (or the notebook itself; many % commands have no shell counterpart).
!cd foo, by itself, has no lasting effect, since the process with the changed directory immediately terminates.
%cd foo changes the current directory of the notebook process, which is a lasting effect.